feat(react): update app and lib generators to support new TS solution setup (#28808)

This PR updates app and lib generators in the following packages such
that they will generate files with the TS solution setup if it is
detected.

- `@nx/react`
- `@nx/next`
- `@nx/remix`
- `@nx/expo`
- `@nx/react-native`

React apps and libs will be linked using npm/pnpm/yarn/bun workspaces
feature rather than through tsconfig paths. This means that local
aliases like `@/` will work with Next.js and Remix apps.

Note: This will be behind `--workspaces` flag when using `npx
create-nx-workspace` and choosing React stack. If you use the None/TS
stack then adding plugins like `nx add @nx/react` then generating apps,
it will automatically pick up the new TS solution setup.


<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
React generators are not compatible with TS solution setup (i.e.
workspaces + TS project references).

## Expected Behavior
React generators work with new TS solution setup (Plain, Next.js, Remix,
Expo, React Native).

## Related Issue(s)
#28322

---------

Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com>
Co-authored-by: Nicholas Cunningham <ndcunningham@gmail.com>
This commit is contained in:
Jack Hsu 2024-11-28 22:18:45 -05:00 committed by GitHub
parent 2cb58b937d
commit ec5a5e6360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
158 changed files with 3456 additions and 657 deletions

View File

@ -28,6 +28,7 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n
| `--defaultBase` | string | Default base to use for new projects. (Default: `main`) |
| `--docker` | boolean | Generate a Dockerfile for the Node API. |
| `--e2eTestRunner` | `playwright`, `cypress`, `none` | Test runner to use for end to end (E2E) tests. |
| `--formatter` | string | Code formatter to use. |
| `--framework` | string | Framework option to be used with certain stacks. |
| `--help` | boolean | Show help. |
| `--interactive` | boolean | Enable interactive mode with presets. (Default: `true`) |
@ -45,6 +46,7 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n
| `--style` | string | Stylesheet type to be used with certain stacks. |
| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) |
| `--version` | boolean | Show version number. |
| `--workspaces` | boolean | Use package manager workspaces. (Default: `false`) |
| `--workspaceType` | `integrated`, `package-based`, `standalone` | The type of workspace to create. |
## Presets

View File

@ -44,13 +44,15 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",
@ -71,7 +73,8 @@
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["playwright", "cypress", "detox", "none"],
"default": "none"
"default": "none",
"x-priority": "important"
},
"standaloneConfig": {
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",

View File

@ -29,13 +29,16 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",

View File

@ -63,8 +63,10 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
"enum": ["eslint", "none"],
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"skipFormat": {
"description": "Skip formatting files.",
@ -76,7 +78,9 @@
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"default": "none",
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"e2eTestRunner": {
"type": "string",

View File

@ -56,17 +56,29 @@
]
}
},
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
"enum": ["eslint", "none"],
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "vitest"
"default": "none",
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"tags": {
"type": "string",
@ -99,7 +111,8 @@
"buildable": {
"type": "boolean",
"default": false,
"description": "Generate a buildable library."
"description": "Generate a buildable library that uses rollup to bundle.",
"x-deprecated": "Use the `bundler` option for greater control (none, vite, rollup)."
},
"importPath": {
"type": "string",

View File

@ -28,6 +28,7 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n
| `--defaultBase` | string | Default base to use for new projects. (Default: `main`) |
| `--docker` | boolean | Generate a Dockerfile for the Node API. |
| `--e2eTestRunner` | `playwright`, `cypress`, `none` | Test runner to use for end to end (E2E) tests. |
| `--formatter` | string | Code formatter to use. |
| `--framework` | string | Framework option to be used with certain stacks. |
| `--help` | boolean | Show help. |
| `--interactive` | boolean | Enable interactive mode with presets. (Default: `true`) |
@ -45,6 +46,7 @@ Install `create-nx-workspace` globally to invoke the command directly, or use `n
| `--style` | string | Stylesheet type to be used with certain stacks. |
| `--useGitHub` | boolean | Will you be using GitHub as your git hosting provider? (Default: `false`) |
| `--version` | boolean | Show version number. |
| `--workspaces` | boolean | Use package manager workspaces. (Default: `false`) |
| `--workspaceType` | `integrated`, `package-based`, `standalone` | The type of workspace to create. |
## Presets

View File

@ -44,13 +44,15 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",
@ -71,7 +73,8 @@
"description": "Adds the specified e2e test runner.",
"type": "string",
"enum": ["playwright", "cypress", "detox", "none"],
"default": "playwright"
"default": "none",
"x-priority": "important"
},
"install": {
"type": "boolean",

View File

@ -32,13 +32,16 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",

View File

@ -74,12 +74,6 @@
]
}
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"routing": {
"type": "boolean",
"description": "Generate application with routes.",
@ -98,11 +92,29 @@
"default": false,
"x-priority": "internal"
},
"bundler": {
"description": "The bundler to use.",
"type": "string",
"enum": ["vite", "webpack", "rspack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "vite",
"x-priority": "important"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "vitest"
"default": "none",
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"inSourceTests": {
"type": "boolean",
@ -165,14 +177,6 @@
"default": false,
"hidden": true
},
"bundler": {
"description": "The bundler to use.",
"type": "string",
"enum": ["vite", "webpack", "rspack"],
"x-prompt": "Which bundler do you want to use to build the application?",
"default": "vite",
"x-priority": "important"
},
"minimal": {
"description": "Generate a React app with a minimal setup, no separate test files.",
"type": "boolean",

View File

@ -65,18 +65,29 @@
]
}
},
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"default": "vitest",
"default": "none",
"description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?"
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"inSourceTests": {
"type": "boolean",
@ -148,14 +159,6 @@
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
"default": false
},
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"compiler": {
"type": "string",
"enum": ["babel", "swc"],

View File

@ -24,19 +24,22 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"default": "vitest",
"default": "none",
"description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?"
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"e2eTestRunner": {
"type": "string",
"enum": ["playwright", "cypress", "none"],
"default": "playwright",
"default": "none",
"description": "Test runner to use for e2e tests",
"x-prompt": "Which E2E test runner would you like to use?"
},

View File

@ -37,17 +37,29 @@
"enum": ["none", "css"],
"default": "css"
},
"buildable": {
"type": "boolean",
"description": "Should the library be buildable?",
"default": false
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"description": "Test Runner to use for Unit Tests",
"x-prompt": "What test runner should be used?",
"default": "vitest"
"default": "none",
"x-priority": "important"
},
"importPath": {
"type": "string",
@ -63,6 +75,12 @@
"description": "Skip formatting files after generator runs",
"default": false,
"x-priority": "internal"
},
"buildable": {
"type": "boolean",
"default": false,
"description": "Generate a buildable library that uses rollup to bundle.",
"x-deprecated": "Use the `bundler` option for greater control (none, vite, rollup)."
}
},
"required": ["directory"],

View File

@ -119,7 +119,7 @@
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file.",
"default": true
"default": false
},
"fileReplacements": {
"description": "Replace files with other files in the build.",

View File

@ -47,7 +47,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"packageManager": {
@ -89,6 +89,11 @@
"type": "string",
"enum": ["none", "prettier"],
"default": "none"
},
"workspaces": {
"description": "Whether to use package manager workspaces.",
"type": "boolean",
"default": false
}
},
"additionalProperties": true,

View File

@ -17,7 +17,7 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"enum": ["eslint", "none"],
"default": "eslint"
},
"routing": {
@ -100,6 +100,17 @@
"prefix": {
"description": "The prefix to use for Angular component and directive selectors.",
"type": "string"
},
"formatter": {
"description": "The tool to use for code formatting.",
"type": "string",
"enum": ["none", "prettier"],
"default": "none"
},
"workspaces": {
"description": "Whether to use package manager workspaces.",
"type": "boolean",
"default": false
}
},
"required": ["preset", "name"],

View File

@ -27,10 +27,13 @@ describe('Linter (legacy)', () => {
newProject({
packages: ['@nx/react', '@nx/js', '@nx/eslint'],
});
runCLI(`generate @nx/react:app apps/${myapp} --tags=validtag`, {
runCLI(
`generate @nx/react:app apps/${myapp} --tags=validtag --linter=eslint`,
{
env: { NX_ADD_PLUGINS: 'false' },
});
runCLI(`generate @nx/js:lib apps/${mylib}`, {
}
);
runCLI(`generate @nx/js:lib apps/${mylib} --linter=eslint`, {
env: { NX_ADD_PLUGINS: 'false' },
});
});
@ -135,10 +138,10 @@ describe('Linter (legacy)', () => {
bundler: 'vite',
e2eTestRunner: 'none',
});
runCLI(`generate @nx/js:lib libs/${mylib}`, {
runCLI(`generate @nx/js:lib libs/${mylib} --linter=eslint`, {
env: { NX_ADD_PLUGINS: 'false' },
});
runCLI(`generate @nx/js:lib libs/${mylib2}`, {
runCLI(`generate @nx/js:lib libs/${mylib2} --linter=eslint`, {
env: { NX_ADD_PLUGINS: 'false' },
});
@ -190,7 +193,7 @@ describe('Linter (legacy)', () => {
bundler: 'vite',
e2eTestRunner: 'none',
});
runCLI(`generate @nx/js:lib ${mylib}`, {
runCLI(`generate @nx/js:lib ${mylib} --linter=eslint`, {
env: { NX_ADD_PLUGINS: 'false' },
});

View File

@ -35,8 +35,10 @@ describe('Linter', () => {
projScope = newProject({
packages: ['@nx/react', '@nx/js', '@nx/eslint'],
});
runCLI(`generate @nx/react:app apps/${myapp} --tags=validtag`);
runCLI(`generate @nx/js:lib libs/${mylib}`);
runCLI(
`generate @nx/react:app apps/${myapp} --tags=validtag --linter eslint --unitTestRunner vitest`
);
runCLI(`generate @nx/js:lib libs/${mylib} --linter eslint`);
});
afterAll(() => cleanupProject());
@ -218,10 +220,14 @@ describe('Linter', () => {
const invalidtaglib = uniq('invalidtaglib');
const validtaglib = uniq('validtaglib');
runCLI(`generate @nx/react:app apps/${myapp2}`);
runCLI(`generate @nx/react:lib libs/${lazylib}`);
runCLI(`generate @nx/js:lib libs/${invalidtaglib} --tags=invalidtag`);
runCLI(`generate @nx/js:lib libs/${validtaglib} --tags=validtag`);
runCLI(`generate @nx/react:app apps/${myapp2} --linter eslint`);
runCLI(`generate @nx/react:lib libs/${lazylib} --linter eslint`);
runCLI(
`generate @nx/js:lib libs/${invalidtaglib} --linter eslint --tags=invalidtag`
);
runCLI(
`generate @nx/js:lib libs/${validtaglib} --linter eslint --tags=validtag`
);
const eslint = readJson('.eslintrc.json');
eslint.overrides[0].rules[
@ -283,9 +289,15 @@ describe('Linter', () => {
beforeAll(() => {
// make these libs non-buildable to avoid dep-checks triggering lint errors
runCLI(`generate @nx/js:lib libs/${libA} --bundler=none`);
runCLI(`generate @nx/js:lib libs/${libB} --bundler=none`);
runCLI(`generate @nx/js:lib libs/${libC} --bundler=none`);
runCLI(
`generate @nx/js:lib libs/${libA} --bundler=none --linter eslint`
);
runCLI(
`generate @nx/js:lib libs/${libB} --bundler=none --linter eslint`
);
runCLI(
`generate @nx/js:lib libs/${libC} --bundler=none --linter eslint`
);
/**
* create tslib-a structure
@ -599,8 +611,8 @@ describe('Linter', () => {
const reactLib = uniq('react-lib');
const jsLib = uniq('js-lib');
runCLI(`generate @nx/react:lib ${reactLib}`);
runCLI(`generate @nx/js:lib ${jsLib}`);
runCLI(`generate @nx/react:lib ${reactLib} --linter eslint`);
runCLI(`generate @nx/js:lib ${jsLib} --linter eslint`);
checkFilesExist(
`${reactLib}/eslint.config.js`,
@ -687,7 +699,7 @@ describe('Linter', () => {
const mylib = uniq('mylib');
runCLI(
`generate @nx/react:app --name=${myapp} --unitTestRunner=jest --directory="."`
`generate @nx/react:app --name=${myapp} --unitTestRunner=jest --linter eslint --directory="."`
);
verifySuccessfulStandaloneSetup(myapp);
@ -701,7 +713,9 @@ describe('Linter', () => {
let e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).toContain('plugin:@nx/javascript');
runCLI(`generate @nx/js:lib libs/${mylib} --unitTestRunner=jest`);
runCLI(
`generate @nx/js:lib libs/${mylib} --unitTestRunner=jest --linter eslint`
);
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
@ -721,7 +735,7 @@ describe('Linter', () => {
const mylib = uniq('mylib');
runCLI(
`generate @nx/angular:app --name=${myapp} --directory="." --no-interactive`
`generate @nx/angular:app --name=${myapp} --directory="." --linter eslint --no-interactive`
);
verifySuccessfulStandaloneSetup(myapp);
@ -734,7 +748,9 @@ describe('Linter', () => {
let e2eOverrides = JSON.stringify(e2eEslint.overrides);
expect(e2eOverrides).toContain('plugin:@nx/javascript');
runCLI(`generate @nx/js:lib libs/${mylib} --no-interactive`);
runCLI(
`generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive`
);
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);
@ -752,7 +768,7 @@ describe('Linter', () => {
const mylib = uniq('mylib');
runCLI(
`generate @nx/node:app --name=${myapp} --directory="." --no-interactive`
`generate @nx/node:app --name=${myapp} --linter eslint --directory="." --no-interactive`
);
verifySuccessfulStandaloneSetup(myapp);
@ -767,7 +783,9 @@ describe('Linter', () => {
expect(e2eOverrides).toContain('plugin:@nx/javascript');
expect(e2eOverrides).toContain('plugin:@nx/typescript');
runCLI(`generate @nx/js:lib libs/${mylib} --no-interactive`);
runCLI(
`generate @nx/js:lib libs/${mylib} --linter eslint --no-interactive`
);
verifySuccessfulMigratedSetup(myapp, mylib);
appEslint = readJson(`.eslintrc.json`);

View File

@ -40,10 +40,10 @@ describe('@nx/expo (legacy)', () => {
return nxJson;
});
runCLI(
`generate @nx/expo:application apps/${appName} --e2eTestRunner=cypress --no-interactive`
`generate @nx/expo:application apps/${appName} --e2eTestRunner=cypress --no-interactive --unitTestRunner=jest --linter=eslint`
);
runCLI(
`generate @nx/expo:library libs/${libName} --buildable --publishable --importPath=${proj}/${libName}`
`generate @nx/expo:library libs/${libName} --buildable --publishable --importPath=${proj}/${libName} --unitTestRunner=jest --linter=eslint`
);
});
afterAll(() => {
@ -210,7 +210,9 @@ describe('@nx/expo (legacy)', () => {
const appName = uniq('app1');
const libName = uniq('@my-org/lib1');
runCLI(`generate @nx/expo:application ${appName} --no-interactive`);
runCLI(
`generate @nx/expo:application ${appName} --no-interactive --unitTestRunner=jest --linter=eslint`
);
// check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided
@ -221,7 +223,9 @@ describe('@nx/expo (legacy)', () => {
`Successfully ran target test for project ${appName}`
);
runCLI(`generate @nx/expo:library ${libName} --buildable`);
runCLI(
`generate @nx/expo:library ${libName} --buildable --unitTestRunner=jest --linter=eslint`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
@ -274,7 +278,7 @@ describe('@nx/expo (legacy)', () => {
it('should run e2e for playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/expo:application ${appName2} --e2eTestRunner=playwright --no-interactive`
`generate @nx/expo:application ${appName2} --e2eTestRunner=playwright --no-interactive --unitTestRunner=jest --linter=eslint`
);
if (runE2ETests()) {
const results = runCLI(`e2e ${appName2}-e2e`, { verbose: true });

View File

@ -23,7 +23,9 @@ describe('@nx/expo', () => {
beforeAll(() => {
newProject();
appName = uniq('app');
runCLI(`generate @nx/expo:app ${appName} --no-interactive`);
runCLI(
`generate @nx/expo:app ${appName} --no-interactive --unitTestRunner=jest --linter=eslint`
);
});
afterAll(() => cleanupProject());
@ -152,7 +154,7 @@ describe('@nx/expo', () => {
it('should create storybook with application', async () => {
runCLI(
`generate @nx/react:storybook-configuration ${appName} --generateStories --no-interactive`
`generate @nx/react:storybook-configuration ${appName} --generateStories --no-interactive --unitTestRunner=jest --linter=eslint`
);
checkFilesExist(
`${appName}/.storybook/main.ts`,

View File

@ -10,7 +10,7 @@ describe('Jest root projects', () => {
packages: ['@nx/angular'],
});
runCLI(
`generate @nx/angular:app --name=${myapp} --directory . --rootProject --no-interactive`
`generate @nx/angular:app --name=${myapp} --directory . --rootProject --no-interactive --unitTestRunner=jest --linter=eslint`
);
});
@ -19,7 +19,9 @@ describe('Jest root projects', () => {
}, 300_000);
it('should add lib project and tests should still work', async () => {
runCLI(`generate @nx/angular:lib ${mylib} --no-interactive`);
runCLI(
`generate @nx/angular:lib ${mylib} --no-interactive --unitTestRunner=jest --linter=eslint`
);
expect(() => runCLI(`test ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow();
@ -32,7 +34,7 @@ describe('Jest root projects', () => {
packages: ['@nx/react'],
});
runCLI(
`generate @nx/react:app --name=${myapp} --directory . --rootProject`
`generate @nx/react:app --name=${myapp} --directory . --rootProject --unitTestRunner=jest --linter=eslint`
);
});
@ -41,7 +43,9 @@ describe('Jest root projects', () => {
}, 300_000);
it('should add lib project and tests should still work', async () => {
runCLI(`generate @nx/react:lib ${mylib} --unitTestRunner=jest`);
runCLI(
`generate @nx/react:lib ${mylib} --unitTestRunner=jest --linter=eslint`
);
expect(() => runCLI(`test ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow();

View File

@ -32,7 +32,7 @@ describe('Next.js Styles', () => {
const lessApp = uniq('app');
runCLI(
`generate @nx/next:app ${lessApp} --no-interactive --style=less --appDir=false --src=false`
`generate @nx/next:app ${lessApp} --no-interactive --style=less --appDir=false --src=false --unitTestRunner=jest --linter=eslint`
);
await checkApp(lessApp, {
@ -44,7 +44,7 @@ describe('Next.js Styles', () => {
const scApp = uniq('app');
runCLI(
`generate @nx/next:app ${scApp} --no-interactive --style=styled-components --appDir=false`
`generate @nx/next:app ${scApp} --no-interactive --style=styled-components --appDir=false --unitTestRunner=jest --linter=eslint`
);
await checkApp(scApp, {
@ -56,7 +56,7 @@ describe('Next.js Styles', () => {
const scAppWithAppRouter = uniq('app');
runCLI(
`generate @nx/next:app ${scAppWithAppRouter} --no-interactive --style=styled-components --appDir=true`
`generate @nx/next:app ${scAppWithAppRouter} --no-interactive --style=styled-components --appDir=true --unitTestRunner=jest --linter=eslint`
);
await checkApp(scAppWithAppRouter, {
@ -68,7 +68,7 @@ describe('Next.js Styles', () => {
const emotionApp = uniq('app');
runCLI(
`generate @nx/next:app ${emotionApp} --no-interactive --style=@emotion/styled --appDir=false`
`generate @nx/next:app ${emotionApp} --no-interactive --style=@emotion/styled --appDir=false --unitTestRunner=jest --linter=eslint`
);
await checkApp(emotionApp, {
@ -83,7 +83,7 @@ describe('Next.js Styles', () => {
const tailwindApp = uniq('app');
runCLI(
`generate @nx/next:app ${tailwindApp} --no-interactive --style=tailwind --appDir=false --src=false`
`generate @nx/next:app ${tailwindApp} --no-interactive --style=tailwind --appDir=false --src=false --unitTestRunner=jest --linter=eslint`
);
await checkApp(tailwindApp, {
@ -107,7 +107,7 @@ describe('Next.js Styles', () => {
const tailwindApp = uniq('app');
runCLI(
`generate @nx/next:app ${tailwindApp} --no-interactive --style=tailwind --appDir=true --src=false`
`generate @nx/next:app ${tailwindApp} --no-interactive --style=tailwind --appDir=true --src=false --unitTestRunner=jest --linter=eslint`
);
await checkApp(tailwindApp, {

View File

@ -37,7 +37,9 @@ describe('Next.js Applications', () => {
const appName = uniq('app1');
const libName = uniq('@my-org/lib1');
runCLI(`generate @nx/next:app ${appName} --no-interactive`);
runCLI(
`generate @nx/next:app ${appName} --no-interactive --linter=eslint --unitTestRunner=jest`
);
// check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided
@ -52,7 +54,9 @@ describe('Next.js Applications', () => {
`Successfully ran target test for project ${appName}`
);
runCLI(`generate @nx/next:lib ${libName} --buildable --no-interactive`);
runCLI(
`generate @nx/next:lib ${libName} --buildable --no-interactive --linter=eslint --unitTestRunner=jest`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
@ -67,7 +71,7 @@ describe('Next.js Applications', () => {
const appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false`
`generate @nx/next:app ${appName} --no-interactive --style=css --appDir=false --linter=eslint --unitTestRunner=jest`
);
checkFilesDoNotExist(`${appName}/.next/build-manifest.json`);
@ -82,7 +86,7 @@ describe('Next.js Applications', () => {
const appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --no-interactive --js --appDir=false --e2eTestRunner=playwright`
`generate @nx/next:app ${appName} --no-interactive --js --appDir=false --e2eTestRunner=playwright --linter=eslint --unitTestRunner=jest`
);
checkFilesExist(`${appName}/src/pages/index.js`);
@ -97,7 +101,7 @@ describe('Next.js Applications', () => {
const libName = uniq('lib');
runCLI(
`generate @nx/next:lib ${libName} --no-interactive --style=none --js`
`generate @nx/next:lib ${libName} --no-interactive --style=none --js --linter=eslint --unitTestRunner=jest`
);
const mainPath = `${appName}/src/pages/index.js`;
@ -133,7 +137,9 @@ describe('Next.js Applications', () => {
it('should support --no-swc flag', async () => {
const appName = uniq('app');
runCLI(`generate @nx/next:app ${appName} --no-interactive --no-swc`);
runCLI(
`generate @nx/next:app ${appName} --no-interactive --no-swc --linter=eslint --unitTestRunner=jest`
);
// Next.js enables SWC when custom .babelrc is not provided.
checkFilesExist(`${appName}/.babelrc`);
@ -148,7 +154,9 @@ describe('Next.js Applications', () => {
it('should support --custom-server flag (swc)', async () => {
const appName = uniq('app');
runCLI(`generate @nx/next:app ${appName} --no-interactive --custom-server`);
runCLI(
`generate @nx/next:app ${appName} --no-interactive --custom-server --linter=eslint --unitTestRunner=jest`
);
checkFilesExist(`${appName}/server/main.ts`);
@ -165,7 +173,7 @@ describe('Next.js Applications', () => {
const appName = uniq('app');
runCLI(
`generate @nx/next:app ${appName} --swc=false --no-interactive --custom-server`
`generate @nx/next:app ${appName} --swc=false --no-interactive --custom-server --linter=eslint --unitTestRunner=jest`
);
checkFilesExist(`${appName}/server/main.ts`);
@ -182,7 +190,9 @@ describe('Next.js Applications', () => {
it('should run e2e-ci test', async () => {
const appName = uniq('app');
runCLI(`generate @nx/next:app ${appName} --no-interactive --style=css`);
runCLI(
`generate @nx/next:app ${appName} --no-interactive --style=css --linter=eslint --unitTestRunner=jest`
);
if (runE2ETests('playwright')) {
const e2eResults = runCLI(`e2e-ci ${appName}-e2e --verbose`, {
@ -202,9 +212,11 @@ describe('Next.js Applications', () => {
const appName = uniq('app');
const pagesAppName = uniq('pages-app');
runCLI(`generate @nx/next:app ${appName} --style=css --no-interactive`);
runCLI(
`generate @nx/next:app ${pagesAppName} --appDir=false --style=css --no-interactive`
`generate @nx/next:app ${appName} --style=css --no-interactive --linter=eslint --unitTestRunner=jest`
);
runCLI(
`generate @nx/next:app ${pagesAppName} --appDir=false --style=css --no-interactive --linter=eslint --unitTestRunner=jest`
);
const appDirNextEnv = `${appName}/next-env.d.ts`;

View File

@ -19,7 +19,7 @@ describe('@nx/workspace:convert-to-monorepo', () => {
it('should convert a standalone webpack and jest react project to a monorepo (legacy)', async () => {
const reactApp = uniq('reactapp');
runCLI(
`generate @nx/react:app --name=${reactApp} --directory="." --bundler=webpack --unitTestRunner=jest --e2eTestRunner=cypress --no-interactive`,
`generate @nx/react:app --name=${reactApp} --directory="." --bundler=webpack --unitTestRunner=jest --e2eTestRunner=cypress --no-interactive --linter=eslint`,
{
env: {
NX_ADD_PLUGINS: 'false',

View File

@ -32,7 +32,7 @@ describe('@nx/workspace:infer-targets', () => {
// default case, everything is generated with crystal, everything should be skipped
const remixApp = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
`generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
);
const output = runCLI(`generate infer-targets --no-interactive --verbose`);
@ -70,7 +70,7 @@ describe('@nx/workspace:infer-targets', () => {
// default case, everything is generated with crystal, relevant plugins should be skipped
const remixApp = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
`generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
);
const output = runCLI(
@ -116,7 +116,7 @@ describe('@nx/workspace:infer-targets', () => {
// even if we make sure there are executors for remix & remix-e2e, only remix conversions will run with --project option
const remixApp = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${remixApp} --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
`generate @nx/remix:app apps/${remixApp} --linter eslint --unitTestRunner jest --e2eTestRunner=playwright --no-interactive`
);
updateJson('nx.json', (json) => {
@ -167,7 +167,7 @@ describe('@nx/workspace:convert-to-monorepo', () => {
it('should be convert a standalone vite and playwright react project to a monorepo', async () => {
const reactApp = uniq('reactapp');
runCLI(
`generate @nx/react:app --name=${reactApp} --directory="." --rootProject=true --bundler=vite --unitTestRunner vitest --e2eTestRunner=playwright --no-interactive`
`generate @nx/react:app --name=${reactApp} --directory="." --rootProject=true --linter eslint --bundler=vite --unitTestRunner vitest --e2eTestRunner=playwright --no-interactive`
);
runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive');

View File

@ -39,10 +39,10 @@ describe('@nx/react-native (legacy)', () => {
return nxJson;
});
runCLI(
`generate @nx/react-native:application ${appName} --directory=apps/${appName} --bunlder=webpack --e2eTestRunner=cypress --install=false --no-interactive`
`generate @nx/react-native:application ${appName} --directory=apps/${appName} --bundler=webpack --e2eTestRunner=cypress --install=false --no-interactive --unitTestRunner=jest --linter=eslint`
);
runCLI(
`generate @nx/react-native:library ${libName} --directory=libs/${libName} --buildable --publishable --importPath=${proj}/${libName} --no-interactive`
`generate @nx/react-native:library ${libName} --directory=libs/${libName} --buildable --publishable --importPath=${proj}/${libName} --no-interactive --unitTestRunner=jest --linter=eslint`
);
});
afterAll(() => {
@ -265,7 +265,7 @@ describe('@nx/react-native (legacy)', () => {
const libName = uniq('@my-org/lib1');
runCLI(
`generate @nx/react-native:application ${appName} --install=false --no-interactive`
`generate @nx/react-native:application ${appName} --install=false --no-interactive --unitTestRunner=jest --linter=eslint`
);
// check files are generated without the layout directory ("apps/") and
@ -274,7 +274,9 @@ describe('@nx/react-native (legacy)', () => {
// check tests pass
expect(() => runCLI(`test ${appName}`)).not.toThrow();
runCLI(`generate @nx/react-native:library ${libName} --buildable`);
runCLI(
`generate @nx/react-native:library ${libName} --buildable --unitTestRunner=jest --linter=eslint`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
@ -286,7 +288,7 @@ describe('@nx/react-native (legacy)', () => {
it('should run build with vite bundler and e2e with playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/react-native:application ${appName2} --directory=apps/${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`
`generate @nx/react-native:application ${appName2} --directory=apps/${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive --unitTestRunner=jest --linter=eslint`
);
expect(() => runCLI(`build ${appName2}`)).not.toThrow();
if (runE2ETests()) {

View File

@ -18,7 +18,7 @@ describe('@nx/react-native', () => {
newProject();
appName = uniq('app');
runCLI(
`generate @nx/react-native:app ${appName} --install=false --no-interactive`
`generate @nx/react-native:app ${appName} --install=false --no-interactive --unitTestRunner=jest --linter=eslint`
);
});
@ -115,7 +115,7 @@ describe('@nx/react-native', () => {
it('should run build with vite bundler and e2e with playwright', async () => {
const appName2 = uniq('my-app');
runCLI(
`generate @nx/react-native:application ${appName2} --directory=apps/${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive`
`generate @nx/react-native:application ${appName2} --directory=apps/${appName2} --bundler=vite --e2eTestRunner=playwright --install=false --no-interactive --unitTestRunner=jest --linter=eslint`
);
expect(() => runCLI(`build ${appName2}`)).not.toThrow();
if (runE2ETests()) {

View File

@ -22,7 +22,7 @@ describe('Build React applications and libraries with Vite', () => {
const viteApp = uniq('viteapp');
runCLI(
`generate @nx/react:app apps/${viteApp} --bundler=vite --compiler=babel --unitTestRunner=vitest --no-interactive`
`generate @nx/react:app apps/${viteApp} --bundler=vite --compiler=babel --unitTestRunner=vitest --no-interactive --linter=eslint`
);
const appTestResults = await runCLIAsync(`test ${viteApp}`);
@ -43,7 +43,7 @@ describe('Build React applications and libraries with Vite', () => {
const viteApp = uniq('viteapp');
runCLI(
`generate @nx/react:app apps/${viteApp} --bundler=vite --compiler=swc --unitTestRunner=vitest --no-interactive`
`generate @nx/react:app apps/${viteApp} --bundler=vite --compiler=swc --unitTestRunner=vitest --no-interactive --linter=eslint`
);
const appTestResults = await runCLIAsync(`test ${viteApp}`);
@ -65,7 +65,7 @@ describe('Build React applications and libraries with Vite', () => {
const viteLib = uniq('vitelib');
runCLI(
`generate @nx/react:app apps/${viteApp} --bundler=vite --unitTestRunner=vitest --inSourceTests --no-interactive`
`generate @nx/react:app apps/${viteApp} --bundler=vite --unitTestRunner=vitest --inSourceTests --no-interactive --linter=eslint`
);
expect(() => {
checkFilesExist(`apps/${viteApp}/src/app/app.spec.tsx`);
@ -85,7 +85,7 @@ describe('Build React applications and libraries with Vite', () => {
checkFilesExist(`dist/apps/${viteApp}/index.html`);
runCLI(
`generate @nx/react:lib libs/${viteLib} --bundler=vite --inSourceTests --unitTestRunner=vitest --no-interactive`
`generate @nx/react:lib libs/${viteLib} --bundler=vite --inSourceTests --unitTestRunner=vitest --no-interactive --linter=eslint`
);
expect(() => {
checkFilesExist(`libs/${viteLib}/src/lib/${viteLib}.spec.tsx`);
@ -125,7 +125,7 @@ describe('Build React applications and libraries with Vite', () => {
const viteLib = uniq('vitelib');
runCLI(
`generate @nx/react:lib libs/${viteLib} --bundler=vite --no-interactive --unit-test-runner=none`
`generate @nx/react:lib libs/${viteLib} --bundler=vite --no-interactive --unit-test-runner=none --linter=eslint`
);
await runCLIAsync(`build ${viteLib}`);
@ -139,7 +139,7 @@ describe('Build React applications and libraries with Vite', () => {
// Convert non-buildable lib to buildable one
const nonBuildableLib = uniq('nonbuildablelib');
runCLI(
`generate @nx/react:lib libs/${nonBuildableLib} --no-interactive --unitTestRunner=jest`
`generate @nx/react:lib libs/${nonBuildableLib} --no-interactive --unitTestRunner=jest --linter=eslint`
);
runCLI(
`generate @nx/vite:configuration ${nonBuildableLib} --uiFramework=react --no-interactive`
@ -157,7 +157,7 @@ describe('Build React applications and libraries with Vite', () => {
const viteApp = uniq('viteapp');
runCLI(
`generate @nx/react:app apps/${viteApp} --bundler=vite --unitTestRunner=jest --no-interactive`
`generate @nx/react:app apps/${viteApp} --bundler=vite --unitTestRunner=jest --no-interactive --linter=eslint`
);
const appTestResults = await runCLIAsync(`test ${viteApp}`);

View File

@ -34,10 +34,10 @@ describe('React Applications', () => {
const libName = uniq('lib');
runCLI(
`generate @nx/react:app apps/${appName} --name=${appName} --bundler=vite --no-interactive --skipFormat`
`generate @nx/react:app apps/${appName} --name=${appName} --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest`
);
runCLI(
`generate @nx/react:lib libs/${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat`
`generate @nx/react:lib libs/${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint`
);
// Library generated with Vite
@ -68,10 +68,10 @@ describe('React Applications', () => {
const libName = uniq('lib');
runCLI(
`generate @nx/react:app ${appName} --bundler=rspack --unit-test-runner=vitest --no-interactive --skipFormat`
`generate @nx/react:app ${appName} --bundler=rspack --unit-test-runner=vitest --no-interactive --skipFormat --linter=eslint`
);
runCLI(
`generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat`
`generate @nx/react:lib ${libName} --bundler=none --no-interactive --unit-test-runner=vitest --skipFormat --linter=eslint`
);
// Library generated with Vite
@ -109,13 +109,13 @@ describe('React Applications', () => {
const redSvg = `<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" viewBox="0 0 30 30"><rect x="10" y="10" width="10" height="10" fill="red"/></svg>`;
runCLI(
`generate @nx/react:app apps/${appName} --style=css --bundler=webpack --unit-test-runner=jest --no-interactive --skipFormat`
`generate @nx/react:app apps/${appName} --style=css --bundler=webpack --unit-test-runner=jest --no-interactive --skipFormat --linter=eslint`
);
runCLI(
`generate @nx/react:lib libs/${libName} --style=css --no-interactive --unit-test-runner=jest --skipFormat`
`generate @nx/react:lib libs/${libName} --style=css --no-interactive --unit-test-runner=jest --skipFormat --linter=eslint`
);
runCLI(
`generate @nx/react:lib libs/${libWithNoComponents} --no-interactive --no-component --unit-test-runner=jest --skipFormat`
`generate @nx/react:lib libs/${libWithNoComponents} --no-interactive --no-component --unit-test-runner=jest --skipFormat --linter=eslint`
);
// Libs should not include package.json by default
@ -201,7 +201,7 @@ describe('React Applications', () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:app apps/${appName} --routing --bundler=webpack --no-interactive --skipFormat`
`generate @nx/react:app apps/${appName} --routing --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest`
);
runCLI(`build ${appName}`);
@ -218,7 +218,7 @@ describe('React Applications', () => {
const libName = uniq('lib');
runCLI(
`g @nx/react:app apps/${appName} --bundler=webpack --no-interactive --skipFormat`
`g @nx/react:app apps/${appName} --bundler=webpack --no-interactive --skipFormat --unitTestRunner=jest --linter=eslint`
);
runCLI(
`g @nx/react:redux apps/${appName}/src/app/lemon/lemon --skipFormat`
@ -254,7 +254,7 @@ describe('React Applications', () => {
const libName = uniq('@my-org/lib1');
runCLI(
`generate @nx/react:app ${appName} --bundler=webpack --no-interactive --skipFormat`
`generate @nx/react:app ${appName} --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest`
);
// check files are generated without the layout directory ("apps/") and
@ -271,7 +271,7 @@ describe('React Applications', () => {
);
runCLI(
`generate @nx/react:lib ${libName} --unit-test-runner=jest --buildable --no-interactive --skipFormat`
`generate @nx/react:lib ${libName} --unit-test-runner=jest --buildable --no-interactive --skipFormat --linter=eslint`
);
// check files are generated without the layout directory ("libs/") and
@ -293,7 +293,7 @@ describe('React Applications', () => {
xit('should support styled-jsx', async () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:app ${appName} --style=styled-jsx --bundler=vite --no-interactive --skipFormat`
`generate @nx/react:app ${appName} --style=styled-jsx --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest`
);
// update app to use styled-jsx
@ -342,7 +342,7 @@ describe('React Applications', () => {
it('should support tailwind', async () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:app apps/${appName} --style=tailwind --bundler=vite --no-interactive --skipFormat`
`generate @nx/react:app apps/${appName} --style=tailwind --bundler=vite --no-interactive --skipFormat --linter=eslint --unitTestRunner=vitest`
);
// update app to use styled-jsx
@ -386,7 +386,7 @@ describe('React Applications', () => {
it('should be formatted on freshly created apps', async () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:app ${appName} --bundler=webpack --no-interactive`
`generate @nx/react:app ${appName} --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest`
);
const stdout = runCLI(`format:check --projects=${appName}`, {
@ -416,15 +416,15 @@ describe('React Applications', () => {
const plainJsLib = uniq('jslib');
runCLI(
`generate @nx/react:app apps/${appName} --bundler=webpack --unit-test-runner=jest --no-interactive --js --skipFormat`
`generate @nx/react:app apps/${appName} --bundler=webpack --unit-test-runner=jest --no-interactive --js --skipFormat --linter=eslint`
);
runCLI(
`generate @nx/react:lib libs/${libName} --no-interactive --js --unit-test-runner=none --skipFormat`
`generate @nx/react:lib libs/${libName} --no-interactive --js --unit-test-runner=none --skipFormat --linter=eslint`
);
// Make sure plain JS libs can be imported as well.
// There was an issue previously: https://github.com/nrwl/nx/issues/10990
runCLI(
`generate @nx/js:lib libs/${plainJsLib} --js --unit-test-runner=none --bundler=none --compiler=tsc --no-interactive --skipFormat`
`generate @nx/js:lib libs/${plainJsLib} --js --unit-test-runner=none --bundler=none --compiler=tsc --no-interactive --skipFormat --linter=eslint`
);
const mainPath = `apps/${appName}/src/main.js`;
@ -450,7 +450,7 @@ describe('React Applications', () => {
`('should support global and css modules', async ({ style }) => {
const appName = uniq('app');
runCLI(
`generate @nx/react:app apps/${appName} --style=${style} --bundler=webpack --no-interactive --skipFormat`
`generate @nx/react:app apps/${appName} --style=${style} --bundler=webpack --no-interactive --skipFormat --linter=eslint --unitTestRunner=jest`
);
// make sure stylePreprocessorOptions works

View File

@ -26,7 +26,9 @@ describe('Remix E2E Tests', () => {
it('should not cause peer dependency conflicts', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:app ${plugin}`);
runCLI(
`generate @nx/remix:app ${plugin} --linter=eslint --unitTestRunner=vitest`
);
await runCommandAsync('npm install');
}, 120000);
@ -43,7 +45,9 @@ describe('Remix E2E Tests', () => {
it('should create app', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:app ${plugin}`);
runCLI(
`generate @nx/remix:app ${plugin} --linter=eslint --unitTestRunner=vitest`
);
const buildResult = runCLI(`build ${plugin}`);
expect(buildResult).toContain('Successfully ran target build');
@ -56,7 +60,7 @@ describe('Remix E2E Tests', () => {
it('should create src in the specified directory', async () => {
const plugin = uniq('remix');
runCLI(
`generate @nx/remix:app --name=${plugin} --directory=subdir --rootProject=false --no-interactive`
`generate @nx/remix:app --name=${plugin} --directory=subdir --rootProject=false --no-interactive --linter=eslint --unitTestRunner=vitest`
);
const result = runCLI(`build ${plugin}`);
@ -69,7 +73,7 @@ describe('Remix E2E Tests', () => {
it('should add tags to the project', async () => {
const plugin = uniq('remix');
runCLI(
`generate @nx/remix:app apps/${plugin} --tags e2etag,e2ePackage`
`generate @nx/remix:app apps/${plugin} --tags e2etag,e2ePackage --linter=eslint --unitTestRunner=vitest`
);
const project = readJson(`apps/${plugin}/project.json`);
expect(project.tags).toEqual(['e2etag', 'e2ePackage']);
@ -79,7 +83,9 @@ describe('Remix E2E Tests', () => {
describe('--js', () => {
it('should create js app and build correctly', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:app ${plugin} --js=true`);
runCLI(
`generate @nx/remix:app ${plugin} --js=true --linter=eslint --unitTestRunner=vitest`
);
const result = runCLI(`build ${plugin}`);
expect(result).toContain('Successfully ran target build');
@ -89,7 +95,9 @@ describe('Remix E2E Tests', () => {
describe('--unitTestRunner', () => {
it('should generate a library with vitest and test correctly', async () => {
const plugin = uniq('remix');
runCLI(`generate @nx/remix:library ${plugin} --unitTestRunner=vitest`);
runCLI(
`generate @nx/remix:library ${plugin} --unitTestRunner=vitest --linter=eslint`
);
const result = runCLI(`test ${plugin}`);
expect(result).toContain(`Successfully ran target test`);
@ -98,11 +106,11 @@ describe('Remix E2E Tests', () => {
it('should generate a library with jest and test correctly', async () => {
const reactapp = uniq('react');
runCLI(
`generate @nx/react:application ${reactapp} --unitTestRunner=jest`
`generate @nx/react:application ${reactapp} --unitTestRunner=jest --linter=eslint`
);
const plugin = uniq('remix');
runCLI(
`generate @nx/remix:application ${plugin} --unitTestRunner=jest`
`generate @nx/remix:application ${plugin} --unitTestRunner=jest --linter=eslint`
);
const result = runCLI(`test ${plugin}`);
@ -118,7 +126,7 @@ describe('Remix E2E Tests', () => {
beforeAll(async () => {
runCLI(
`generate @nx/remix:app apps/${plugin} --tags e2etag,e2ePackage`
`generate @nx/remix:app apps/${plugin} --tags e2etag,e2ePackage --linter=eslint --unitTestRunner=vitest`
);
}, 120000);

View File

@ -67,9 +67,9 @@ describe('rspack e2e', () => {
// Make sure expected files are present.
/**
* The files that are generated are:
* ["3rdpartylicenses.txt", "assets", "favicon.ico", "index.html", "main.bf7851e6.js", "runtime.e4294127.js"]
* ["assets", "favicon.ico", "index.html", "main.bf7851e6.js", "runtime.e4294127.js"]
*/
expect(listFiles(`dist/${project}`)).toHaveLength(6);
expect(listFiles(`dist/${project}`)).toHaveLength(5);
result = runCLI(`test ${project}`);
expect(result).toContain('Successfully ran target test');
@ -87,7 +87,7 @@ describe('rspack e2e', () => {
env: { NODE_ENV: 'production' },
});
expect(result).toContain('Successfully ran target build');
expect(listFiles(`dist/${project}`)).toHaveLength(6); // same length as before
expect(listFiles(`dist/${project}`)).toHaveLength(5); // same length as before
// Generate a new app and check that the files are correct
const app2 = uniq('app2');
@ -120,7 +120,7 @@ describe('rspack e2e', () => {
});
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${app2}`)).toHaveLength(6);
expect(listFiles(`dist/${app2}`)).toHaveLength(5);
result = runCLI(`test ${app2}`);
expect(result).toContain('Successfully ran target test');
@ -139,11 +139,11 @@ describe('rspack e2e', () => {
result = runCLI(`build ${app3}`);
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${app3}`)).toHaveLength(3);
expect(listFiles(`dist/${app3}`)).toHaveLength(2);
result = runCLI(`build ${app3} --generatePackageJson=true`);
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${app3}`)).toHaveLength(5);
expect(listFiles(`dist/${app3}`)).toHaveLength(4);
}, 200_000);
});

View File

@ -222,6 +222,8 @@ export function runCreateWorkspace(
docker,
nextAppDir,
nextSrcDir,
linter = 'eslint',
formatter = 'prettier',
e2eTestRunner,
ssr,
framework,
@ -241,7 +243,9 @@ export function runCreateWorkspace(
docker?: boolean;
nextAppDir?: boolean;
nextSrcDir?: boolean;
linter?: 'none' | 'eslint';
e2eTestRunner?: 'cypress' | 'playwright' | 'jest' | 'detox' | 'none';
formatter?: 'prettier' | 'none';
ssr?: boolean;
framework?: string;
prefix?: string;
@ -291,6 +295,14 @@ export function runCreateWorkspace(
command += ` --package-manager=${packageManager}`;
}
if (linter) {
command += ` --linter=${linter}`;
}
if (formatter) {
command += ` --formatter=${formatter}`;
}
if (e2eTestRunner) {
command += ` --e2eTestRunner=${e2eTestRunner}`;
}

View File

@ -48,7 +48,9 @@ describe('Vite Plugin', () => {
beforeAll(() => {
myApp = uniq('my-app');
runCLI(`generate @nx/react:app ${myApp} --bundler=vite`);
runCLI(
`generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest`
);
});
afterEach(() => {
@ -95,7 +97,7 @@ describe('Vite Plugin', () => {
beforeEach(() => {
myApp = uniq('my-app');
runCLI(
`generate @nx/web:app ${myApp} --bundler=vite --directory=${myApp}`
`generate @nx/web:app ${myApp} --bundler=vite --unitTestRunner=vitest --directory=${myApp}`
);
});
it('should build application', async () => {
@ -187,7 +189,7 @@ describe('Vite Plugin', () => {
packages: ['@nx/react'],
});
runCLI(
`generate @nx/react:app ${app} --bundler=vite --no-interactive --directory=${app}`
`generate @nx/react:app ${app} --bundler=vite --unitTestRunner=vitest --no-interactive --directory=${app}`
);
// only this project will be directly used from dist

View File

@ -24,10 +24,10 @@ describe('Webpack Plugin (legacy)', () => {
packages: ['@nx/react'],
});
runCLI(
`generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --rootProject --no-interactive`
`generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --rootProject --no-interactive --unitTestRunner=jest --linter=eslint`
);
runCLI(
`generate @nx/react:lib ${libName} --unitTestRunner jest --no-interactive`
`generate @nx/react:lib ${libName} --unitTestRunner jest --no-interactive --linter=eslint`
);
});
@ -72,7 +72,9 @@ describe('Webpack Plugin (legacy)', () => {
// Issue: https://github.com/nrwl/nx/issues/20179
it('should allow main/styles entries to be spread within composePlugins() function (#20179)', () => {
const appName = uniq('app');
runCLI(`generate @nx/web:app ${appName} --bundler webpack`);
runCLI(
`generate @nx/web:app ${appName} --bundler webpack --unitTestRunner=jest --linter=eslint`
);
checkFilesExist(`${appName}/src/main.ts`);
updateFile(`${appName}/src/main.ts`, `console.log('Hello');\n`);
@ -109,7 +111,7 @@ describe('Webpack Plugin (legacy)', () => {
it('should support standard webpack config with executors', () => {
const appName = uniq('app');
runCLI(
`generate @nx/web:app ${appName} --bundler webpack --e2eTestRunner=playwright`
`generate @nx/web:app ${appName} --bundler webpack --e2eTestRunner=playwright --unitTestRunner=jest --linter=eslint`
);
updateFile(
`${appName}/src/main.ts`,
@ -153,7 +155,7 @@ describe('Webpack Plugin (legacy)', () => {
it('should convert withNx webpack config to a standard config using NxWebpackPlugin', () => {
const appName = 'app3224373'; // Needs to be reserved so that the snapshot projectName matches
runCLI(
`generate @nx/web:app ${appName} --bundler webpack --e2eTestRunner=playwright`
`generate @nx/web:app ${appName} --bundler webpack --e2eTestRunner=playwright --unitTestRunner=vitest --linter=eslint`
);
updateFile(
`${appName}/src/main.ts`,

View File

@ -52,6 +52,9 @@ interface ReactArguments extends BaseArguments {
nextAppDir: boolean;
nextSrcDir: boolean;
e2eTestRunner: 'none' | 'cypress' | 'playwright';
linter?: 'none' | 'eslint';
formatter?: 'none' | 'prettier';
workspaces?: boolean;
}
interface AngularArguments extends BaseArguments {
@ -157,6 +160,15 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
describe: chalk.dim`Bundler to be used to build the app.`,
type: 'string',
})
.option('workspaces', {
describe: chalk.dim`Use package manager workspaces.`,
type: 'boolean',
default: false,
})
.option('formatter', {
describe: chalk.dim`Code formatter to use.`,
type: 'string',
})
.option('framework', {
describe: chalk.dim`Framework option to be used with certain stacks.`,
type: 'string',
@ -440,14 +452,11 @@ async function determinePresetOptions(
}
}
async function determineNoneOptions(
parsedArgs: yargs.Arguments<NoneArguments>
): Promise<Partial<NoneArguments>> {
if (
(!parsedArgs.preset || parsedArgs.preset === Preset.TS) &&
process.env.NX_ADD_PLUGINS !== 'false' &&
process.env.NX_ADD_TS_PLUGIN !== 'false'
) {
async function determineFormatterOptions(args: {
formatter?: 'none' | 'prettier';
interactive?: boolean;
}) {
if (args.formatter) return args.formatter;
const reply = await enquirer.prompt<{ prettier: 'Yes' | 'No' }>([
{
name: 'prettier',
@ -462,12 +471,44 @@ async function determineNoneOptions(
},
],
initial: 1,
skip: !parsedArgs.interactive || isCI(),
skip: !args.interactive || isCI(),
},
]);
return reply.prettier === 'Yes' ? 'prettier' : 'none';
}
async function determineLinterOptions(args: { interactive?: boolean }) {
const reply = await enquirer.prompt<{ eslint: 'Yes' | 'No' }>([
{
name: 'eslint',
message: `Would you like to use ESLint?`,
type: 'autocomplete',
choices: [
{
name: 'Yes',
},
{
name: 'No',
},
],
initial: 1,
skip: !args.interactive || isCI(),
},
]);
return reply.eslint === 'Yes' ? 'eslint' : 'none';
}
async function determineNoneOptions(
parsedArgs: yargs.Arguments<NoneArguments>
): Promise<Partial<NoneArguments>> {
if (
(!parsedArgs.preset || parsedArgs.preset === Preset.TS) &&
process.env.NX_ADD_PLUGINS !== 'false' &&
process.env.NX_ADD_TS_PLUGIN !== 'false'
) {
return {
preset: Preset.TS,
formatter: reply.prettier === 'Yes' ? 'prettier' : 'none',
formatter: await determineFormatterOptions(parsedArgs),
};
} else {
let preset: Preset;
@ -535,6 +576,10 @@ async function determineReactOptions(
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
let nextAppDir = false;
let nextSrcDir = false;
let linter: undefined | 'none' | 'eslint';
let formatter: undefined | 'none' | 'prettier';
const workspaces = parsedArgs.workspaces ?? false;
if (parsedArgs.preset && parsedArgs.preset !== Preset.React) {
preset = parsedArgs.preset;
@ -550,27 +595,25 @@ async function determineReactOptions(
} else {
const framework = await determineReactFramework(parsedArgs);
// React Native and Expo only support integrated monorepos for now.
// TODO(jack): Add standalone support for React Native and Expo.
const workspaceType =
framework === 'react-native' || framework === 'expo'
? 'integrated'
: await determineStandaloneOrMonorepo();
const isStandalone =
workspaces || framework === 'react-native' || framework === 'expo'
? false
: (await determineStandaloneOrMonorepo()) === 'standalone';
if (workspaceType === 'standalone') {
if (isStandalone) {
appName = parsedArgs.name;
} else {
appName = await determineAppName(parsedArgs);
}
if (framework === 'nextjs') {
if (workspaceType === 'standalone') {
if (isStandalone) {
preset = Preset.NextJsStandalone;
} else {
preset = Preset.NextJs;
}
} else if (framework === 'remix') {
if (workspaceType === 'standalone') {
if (isStandalone) {
preset = Preset.RemixStandalone;
} else {
preset = Preset.RemixMonorepo;
@ -580,7 +623,7 @@ async function determineReactOptions(
} else if (framework === 'expo') {
preset = Preset.Expo;
} else {
if (workspaceType === 'standalone') {
if (isStandalone) {
preset = Preset.ReactStandalone;
} else {
preset = Preset.ReactMonorepo;
@ -657,6 +700,14 @@ async function determineReactOptions(
style = reply.style;
}
if (workspaces) {
linter = await determineLinterOptions(parsedArgs);
formatter = await determineFormatterOptions(parsedArgs);
} else {
linter = 'eslint';
formatter = 'prettier';
}
return {
preset,
style,
@ -665,6 +716,9 @@ async function determineReactOptions(
nextAppDir,
nextSrcDir,
e2eTestRunner,
linter,
formatter,
workspaces,
};
}

View File

@ -8,6 +8,7 @@ import { setupCI } from './utils/ci/setup-ci';
import { initializeGitRepo } from './utils/git/git';
import { getPackageNameFromThirdPartyPreset } from './utils/preset/get-third-party-preset';
import { mapErrorToBodyLines } from './utils/error-utils';
import { Preset } from './utils/preset/preset';
export async function createWorkspace<T extends CreateWorkspaceOptions>(
preset: string,
@ -30,12 +31,14 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
const tmpDir = await createSandbox(packageManager);
const workspaceGlobs = getWorkspaceGlobsFromPreset(preset);
// nx new requires a preset currently. We should probably make it optional.
const directory = await createEmptyWorkspace<T>(
tmpDir,
name,
packageManager,
{ ...options, preset }
{ ...options, preset, workspaceGlobs }
);
// If the preset is a third-party preset, we need to call createPreset to install it
@ -96,3 +99,24 @@ export function extractConnectUrl(text: string): string | null {
const match = text.match(urlPattern);
return match ? match[0] : null;
}
function getWorkspaceGlobsFromPreset(preset: string): string[] {
// Should match how apps are created in `packages/workspace/src/generators/preset/preset.ts`.
switch (preset) {
case Preset.AngularMonorepo:
case Preset.Expo:
case Preset.Express:
case Preset.Nest:
case Preset.NextJs:
case Preset.NodeMonorepo:
case Preset.Nuxt:
case Preset.ReactNative:
case Preset.ReactMonorepo:
case Preset.RemixMonorepo:
case Preset.VueMonorepo:
case Preset.WebComponents:
return ['apps/**', 'packages/**'];
default:
return ['packages/**'];
}
}

View File

@ -16,5 +16,6 @@
"<%= offsetFromProjectRoot %>**/*.cy.js",
<%_ if (jsx) { _%>"<%= offsetFromProjectRoot %>**/*.cy.jsx",<%_ } _%>
"<%= offsetFromProjectRoot %>**/*.d.ts"
]
],
"exclude": ["dist"<% if (linter === 'eslint') { %>, "eslint.config.js"<% } %>]
}

View File

@ -105,6 +105,26 @@ export async function configurationGeneratorInternal(
addTarget(tree, opts, projectGraph);
}
const projectTsConfigPath = joinPathFragments(
opts.projectRoot,
'tsconfig.json'
);
if (tree.exists(projectTsConfigPath)) {
updateJson(tree, projectTsConfigPath, (json) => {
// Cypress uses commonjs, unless the project is also using commonjs (or does not set "module" i.e. uses default of commonjs),
// then we need to set the moduleResolution to node10 or else Cypress will fail with TS5095 error.
// See: https://github.com/cypress-io/cypress/issues/27731
if (
(json.compilerOptions?.module ||
json.compilerOptions?.module !== 'commonjs') &&
json.compilerOptions?.moduleResolution
) {
json.compilerOptions.moduleResolution = 'node10';
}
return json;
});
}
const { root: projectRoot } = readProjectConfiguration(tree, options.project);
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
if (isTsSolutionSetup) {
@ -201,6 +221,7 @@ In this case you need to provide a devServerTarget,'<projectName>:<targetName>[:
return {
...options,
bundler: options.bundler ?? 'webpack',
projectRoot: projectConfig.root,
rootProject: options.rootProject ?? projectConfig.root === '.',
linter,
devServerTarget,
@ -408,6 +429,9 @@ function createPackageJson(tree: Tree, options: NormalizedSchema) {
name: importPath,
version: '0.0.1',
private: true,
nx: {
name: options.project,
},
};
writeJson(tree, packageJsonPath, packageJson);
}

View File

@ -1,5 +1,5 @@
import { formatFiles, runTasksInSerial, Tree } from '@nx/devkit';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { initGenerator as jsInitGenerator } from '@nx/js';
import detoxInitGenerator from '../init/init';
import { addGitIgnoreEntry } from './lib/add-git-ignore-entry';
@ -21,7 +21,9 @@ export async function detoxApplicationGeneratorInternal(
host: Tree,
schema: Schema
) {
assertNotUsingTsSolutionSetup(host, 'detox', 'application');
const jsInitTask = await jsInitGenerator(host, {
skipFormat: true,
});
const options = await normalizeOptions(host, schema);
@ -40,7 +42,7 @@ export async function detoxApplicationGeneratorInternal(
await formatFiles(host);
}
return runTasksInSerial(initTask, lintingTask, depsTask);
return runTasksInSerial(jsInitTask, initTask, lintingTask, depsTask);
}
export default detoxApplicationGenerator;

View File

@ -1,8 +1,10 @@
import {
addProjectConfiguration,
joinPathFragments,
readNxJson,
TargetConfiguration,
Tree,
writeJson,
} from '@nx/devkit';
import {
expoBuildTarget,
@ -11,6 +13,7 @@ import {
reactNativeTestTarget,
} from './get-targets';
import { NormalizedSchema } from './normalize-options';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
@ -20,6 +23,19 @@ export function addProject(host: Tree, options: NormalizedSchema) {
: p.plugin === '@nx/detox/plugin'
);
if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.e2eProjectRoot, 'package.json'), {
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
sourceRoot: `${options.e2eProjectRoot}/src`,
projectType: 'application',
targets: hasPlugin ? undefined : getTargets(options),
implicitDependencies: [options.appProject],
},
});
} else {
addProjectConfiguration(host, options.e2eProjectName, {
root: options.e2eProjectRoot,
sourceRoot: `${options.e2eProjectRoot}/src`,
@ -29,6 +45,7 @@ export function addProject(host: Tree, options: NormalizedSchema) {
implicitDependencies: [options.appProject],
});
}
}
function getTargets(options: NormalizedSchema) {
const targets: { [key: string]: TargetConfiguration } = {};

View File

@ -9,7 +9,6 @@ import {
Tree,
} from '@nx/devkit';
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { createNodes } from '../../plugins/plugin';
import { detoxVersion, nxVersion } from '../../utils/versions';
import { Schema } from './schema';
@ -19,8 +18,6 @@ export function detoxInitGenerator(host: Tree, schema: Schema) {
}
export async function detoxInitGeneratorInternal(host: Tree, schema: Schema) {
assertNotUsingTsSolutionSetup(host, 'detox', 'init');
const tasks: GeneratorCallback[] = [];
const nxJson = readNxJson(host);

View File

@ -90,6 +90,7 @@ export async function lintWorkspaceRulesProjectGenerator(
join(workspaceLintPluginDir, 'tsconfig.spec.json'),
(json) => {
delete json.compilerOptions?.module;
delete json.compilerOptions?.moduleResolution;
if (json.include) {
json.include = json.include.map((v) => {

View File

@ -6,6 +6,8 @@ import {
readNxJson,
readProjectConfiguration,
Tree,
updateJson,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
@ -327,4 +329,147 @@ describe('app', () => {
`);
});
});
describe('TS solution setup', () => {
it('should add project references when using TS solution', async () => {
const tree = createTreeWithEmptyWorkspace();
tree.write('.gitignore', '');
updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*'];
return json;
});
writeJson(tree, 'tsconfig.base.json', {
compilerOptions: {
composite: true,
declaration: true,
},
});
writeJson(tree, 'tsconfig.json', {
extends: './tsconfig.base.json',
files: [],
references: [],
});
await expoApplicationGenerator(tree, {
directory: 'my-app',
displayName: 'myApp',
linter: Linter.EsLint,
e2eTestRunner: 'none',
skipFormat: false,
js: false,
unitTestRunner: 'jest',
addPlugin: true,
});
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
[
{
"path": "./my-app",
},
]
`);
expect(readJson(tree, 'my-app/tsconfig.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"jsx": "react-native",
"lib": [
"dom",
"esnext",
],
"moduleResolution": "node",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
},
"extends": "../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json",
},
{
"path": "./tsconfig.spec.json",
},
],
}
`);
expect(readJson(tree, 'my-app/tsconfig.app.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"noUnusedLocals": false,
"outDir": "out-tsc/my-app",
"rootDir": "src",
"types": [
"node",
],
},
"exclude": [
"dist",
"jest.config.ts",
"**/*.spec.ts",
"**/*.spec.tsx",
"src/test-setup.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs",
],
"extends": "../tsconfig.base.json",
"files": [
"../node_modules/@nx/expo/typings/svg.d.ts",
],
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
],
}
`);
expect(readJson(tree, 'my-app/tsconfig.spec.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"noUnusedLocals": false,
"outDir": "./out-tsc/jest",
"types": [
"jest",
"node",
],
},
"extends": "../tsconfig.base.json",
"files": [
"src/test-setup.ts",
],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts",
],
"references": [
{
"path": "./tsconfig.app.json",
},
],
}
`);
});
});
});

View File

@ -6,7 +6,7 @@ import {
Tree,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
@ -36,16 +36,16 @@ export async function expoApplicationGeneratorInternal(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'expo', 'application');
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
});
const options = await normalizeOptions(host, schema);
tasks.push(jsInitTask);
const initTask = await initGenerator(host, { ...options, skipFormat: true });
tasks.push(initTask);
@ -80,6 +80,21 @@ export async function expoApplicationGeneratorInternal(
tasks.push(e2eTask);
addEasScripts(host);
updateTsconfigFiles(
host,
options.appProjectRoot,
'tsconfig.app.json',
{
jsx: 'react-jsx',
module: 'esnext',
moduleResolution: 'bundler',
noUnusedLocals: false,
},
options.linter === 'eslint'
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
: undefined
);
if (!options.skipFormat) {
await formatFiles(host);
}

View File

@ -1,10 +1,11 @@
import { GeneratorCallback, Tree } from '@nx/devkit';
import {
addProjectConfiguration,
ensurePackage,
getPackageManagerCommand,
GeneratorCallback,
joinPathFragments,
readNxJson,
Tree,
writeJson,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
@ -14,6 +15,7 @@ import { NormalizedSchema } from './normalize-options';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { getE2EWebServerInfo } from '@nx/devkit/src/generators/e2e-web-server-info-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function addE2e(
tree: Tree,
@ -40,6 +42,22 @@ export async function addE2e(
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
if (isUsingTsSolutionSetup(tree)) {
writeJson(
tree,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
{
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName],
},
}
);
} else {
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
@ -48,6 +66,7 @@ export async function addE2e(
implicitDependencies: [options.projectName],
tags: [],
});
}
const e2eTask = await configurationGenerator(tree, {
...options,
@ -106,6 +125,22 @@ export async function addE2e(
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
if (isUsingTsSolutionSetup(tree)) {
writeJson(
tree,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
{
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName],
},
}
);
} else {
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
@ -113,6 +148,7 @@ export async function addE2e(
targets: {},
implicitDependencies: [options.projectName],
});
}
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,

View File

@ -1,14 +1,18 @@
import {
addProjectConfiguration,
joinPathFragments,
ProjectConfiguration,
readNxJson,
TargetConfiguration,
Tree,
writeJson,
} from '@nx/devkit';
import { hasExpoPlugin } from '../../../utils/has-expo-plugin';
import { NormalizedSchema } from './normalize-options';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
@ -26,6 +30,21 @@ export function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags,
};
if (isUsingTsSolutionSetup(host)) {
const packageName = getImportPath(host, options.name);
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: packageName,
version: '0.0.1',
private: true,
nx: {
name: packageName === options.name ? undefined : options.name,
projectType: 'application',
sourceRoot: `${options.appProjectRoot}/src`,
targets: hasPlugin ? undefined : getTargets(options),
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
});
} else {
addProjectConfiguration(
host,
options.projectName,
@ -33,6 +52,7 @@ export function addProject(host: Tree, options: NormalizedSchema) {
options.standaloneConfig
);
}
}
function getTargets(options: NormalizedSchema) {
const architect: { [key: string]: TargetConfiguration } = {};

View File

@ -15,6 +15,9 @@ export interface Schema {
e2eTestRunner: 'cypress' | 'playwright' | 'detox' | 'none'; // default is none
standaloneConfig?: boolean;
skipPackageJson?: boolean; // default is false
// Internal options
addPlugin?: boolean;
nxCloudToken?: string;
useTsSolution?: boolean;
formatter?: 'prettier' | 'none';
}

View File

@ -44,13 +44,15 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",
@ -71,7 +73,8 @@
"description": "Adds the specified e2e test runner",
"type": "string",
"enum": ["playwright", "cypress", "detox", "none"],
"default": "none"
"default": "none",
"x-priority": "important"
},
"standaloneConfig": {
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",

View File

@ -9,7 +9,6 @@ import {
Tree,
} from '@nx/devkit';
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { createNodes } from '../../../plugins/plugin';
import {
expoCliVersion,
@ -28,8 +27,6 @@ export function expoInitGenerator(tree: Tree, schema: Schema) {
}
export async function expoInitGeneratorInternal(host: Tree, schema: Schema) {
assertNotUsingTsSolutionSetup(host, 'expo', 'init');
const nxJson = readNxJson(host);
const addPluginDefault =
process.env.NX_ADD_PLUGINS !== 'false' &&

View File

@ -4,6 +4,7 @@ import {
ensureProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export interface NormalizedSchema extends Schema {
name: string;
@ -12,6 +13,7 @@ export interface NormalizedSchema extends Schema {
routePath: string;
parsedTags: string[];
appMain: string;
isUsingTsSolutionConfig: boolean;
}
export async function normalizeOptions(
@ -50,6 +52,7 @@ export async function normalizeOptions(
parsedTags,
importPath,
appMain,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host),
};
return normalized;

View File

@ -233,6 +233,8 @@ describe('lib', () => {
"compilerOptions": {
"outDir": "../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"jsx": "react-jsx",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],

View File

@ -4,6 +4,7 @@ import {
formatFiles,
generateFiles,
GeneratorCallback,
installPackagesTask,
joinPathFragments,
names,
offsetFromRoot,
@ -13,6 +14,7 @@ import {
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import {
@ -20,7 +22,6 @@ import {
getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator,
} from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import init from '../init/init';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
@ -35,6 +36,8 @@ import { ensureDependencies } from '../../utils/ensure-dependencies';
import { initRootBabelConfig } from '../../utils/init-root-babel-config';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export async function expoLibraryGenerator(
host: Tree,
@ -50,7 +53,13 @@ export async function expoLibraryGeneratorInternal(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'expo', 'library');
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema);
if (options.publishable === true && !schema.importPath) {
@ -59,13 +68,6 @@ export async function expoLibraryGeneratorInternal(
);
}
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const initTask = await init(host, { ...options, skipFormat: true });
tasks.push(initTask);
if (!options.skipPackageJson) {
@ -114,10 +116,29 @@ export async function expoLibraryGeneratorInternal(
]);
}
updateTsconfigFiles(
host,
options.projectRoot,
'tsconfig.lib.json',
{
jsx: 'react-jsx',
module: 'esnext',
moduleResolution: 'bundler',
},
options.linter === 'eslint'
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
: undefined
);
if (!options.skipFormat) {
await formatFiles(host);
}
// Always run install to link packages.
if (options.isUsingTsSolutionConfig) {
tasks.push(() => installPackagesTask(host));
}
tasks.push(() => {
logShowProjectCommand(options.name);
});
@ -136,7 +157,29 @@ async function addProject(
tags: options.parsedTags,
targets: {},
};
if (options.isUsingTsSolutionConfig) {
const packageName = getImportPath(host, options.name);
const sourceEntry = !options.buildable
? options.js
? './src/index.js'
: './src/index.ts'
: undefined;
writeJson(host, joinPathFragments(options.projectRoot, 'package.json'), {
name: packageName,
version: '0.0.1',
main: sourceEntry,
types: sourceEntry,
nx: {
name: packageName === options.name ? undefined : options.name,
projectType: 'library',
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
});
} else {
addProjectConfiguration(host, options.name, project);
}
if (!options.publishable && !options.buildable) {
return () => {};

View File

@ -29,13 +29,16 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",

View File

@ -2,8 +2,9 @@
"extends": "<%= extendedConfig %>",
"compilerOptions": {
"outDir": "<%= outDir %>",<% if (module) { %>
"module": "<%= module %>",<% } %>
"types": ["jest", "node"]
"module": "<%= module %>",<% } if (module === 'commonjs') { %>
"moduleResolution": "node10",<% } if (supportTsx) { %>
"jsx": "react-jsx",<% } %>"types": ["jest", "node"]
},<% if(setupFile !== 'none') { %>
"files": ["src/test-setup.ts"],<% } %>
"include": [

View File

@ -4,10 +4,10 @@ import {
readProjectConfiguration,
Tree,
} from '@nx/devkit';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { join } from 'path';
import type { JestPresetExtension } from '../../../utils/config/config-file';
import { NormalizedJestProjectSchema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export function createFiles(
tree: Tree,

View File

@ -8,6 +8,7 @@ import {
readProjectConfiguration,
Tree,
updateJson,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { libraryGenerator } from './library';
@ -1611,4 +1612,64 @@ describe('lib', () => {
`);
});
});
describe('TS solution setup', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*'];
return json;
});
writeJson(tree, 'tsconfig.base.json', {
compilerOptions: {
composite: true,
declaration: true,
},
});
writeJson(tree, 'tsconfig.json', {
extends: './tsconfig.base.json',
files: [],
references: [],
});
});
it('should map non-buildable libraries to source', async () => {
await libraryGenerator(tree, {
...defaultOptions,
directory: 'my-ts-lib',
bundler: 'none',
unitTestRunner: 'none',
linter: 'none',
});
await libraryGenerator(tree, {
...defaultOptions,
directory: 'my-js-lib',
js: true,
bundler: 'none',
unitTestRunner: 'none',
linter: 'none',
});
expect(readJson(tree, 'my-ts-lib/package.json')).toMatchInlineSnapshot(`
{
"dependencies": {},
"main": "./src/index.ts",
"name": "@proj/my-ts-lib",
"private": true,
"types": "./src/index.ts",
"version": "0.0.1",
}
`);
expect(readJson(tree, 'my-js-lib/package.json')).toMatchInlineSnapshot(`
{
"dependencies": {},
"main": "./src/index.js",
"name": "@proj/my-js-lib",
"private": true,
"types": "./src/index.js",
"version": "0.0.1",
}
`);
});
});
});

View File

@ -1,5 +1,6 @@
import {
addDependenciesToPackageJson,
installPackagesTask,
addProjectConfiguration,
ensurePackage,
formatFiles,
@ -234,6 +235,11 @@ export async function libraryGeneratorInternal(
});
}
// Always run install to link packages.
if (options.isUsingTsSolutionConfig) {
tasks.push(() => installPackagesTask(tree));
}
tasks.push(() => {
logShowProjectCommand(options.name);
});
@ -1125,6 +1131,17 @@ function determineEntryFields(
// Safest option is to not set a type field.
// Allow the user to decide which module format their library is using
type: undefined,
// For non-buildable libraries, point to source so we can still use them in apps via bundlers like Vite.
main: options.isUsingTsSolutionConfig
? options.js
? './src/index.js'
: './src/index.ts'
: undefined,
types: options.isUsingTsSolutionConfig
? options.js
? './src/index.js'
: './src/index.ts'
: undefined,
};
}
}

View File

@ -268,7 +268,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`);
});
it('should not invoke tsc with `--emitDeclarationOnly` when `noEmit` is set in the tsconfig.json file', async () => {
it('should not invoke `tsc --build` when `noEmit` is set in the tsconfig.json file', async () => {
// set directly in tsconfig.json file
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({
@ -285,7 +285,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"targets": {
"typecheck": {
"cache": true,
"command": "tsc --build --pretty --verbose",
"command": "echo "The 'typecheck' target is disabled because one or more project references set 'noEmit: true' in their tsconfig. Remove this property to resolve this issue."",
"dependsOn": [
"^typecheck",
],
@ -345,7 +345,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"targets": {
"typecheck": {
"cache": true,
"command": "tsc --build --pretty --verbose",
"command": "echo "The 'typecheck' target is disabled because one or more project references set 'noEmit: true' in their tsconfig. Remove this property to resolve this issue."",
"dependsOn": [
"^typecheck",
],
@ -387,7 +387,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`);
});
it('should not invoke tsc with `--emitDeclarationOnly` when `noEmit` is set in any of the referenced tsconfig.json files', async () => {
it('should not invoke `tsc --build` when `noEmit` is set in any of the referenced tsconfig.json files', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({
files: [],
@ -407,7 +407,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"targets": {
"typecheck": {
"cache": true,
"command": "tsc --build --pretty --verbose",
"command": "echo "The 'typecheck' target is disabled because one or more project references set 'noEmit: true' in their tsconfig. Remove this property to resolve this issue."",
"dependsOn": [
"^typecheck",
],

View File

@ -168,12 +168,23 @@ async function createNodesInternal(
return {};
}
// Do not create project for Next.js projects since they are not compatible with
// project references and typecheck will fail.
if (
siblingFiles.includes('next.config.js') ||
siblingFiles.includes('next.config.cjs') ||
siblingFiles.includes('next.config.mjs') ||
siblingFiles.includes('next.config.ts')
) {
return {};
}
/**
* The cache key is composed by:
* - hashes of the content of the relevant files that can affect what's inferred by the plugin:
* - current config file
* - config files extended by the current config file (recursively up to the root config file)
* - referenced config files that are internal to the owning Nx project of the current config file
* - referenced config files that are internal to the owning Nx project of the current config file, or is a shallow external reference of the owning Nx project
* - lock file
* - hash of the plugin options
* - current config file path
@ -185,11 +196,17 @@ async function createNodesInternal(
context.workspaceRoot,
projectRoot
);
const externalProjectReferences = resolveShallowExternalProjectReferences(
tsConfig,
context.workspaceRoot,
projectRoot
);
const nodeHash = hashArray([
...[
fullConfigPath,
...extendedConfigFiles.files,
...Object.keys(internalReferencedFiles),
...Object.keys(externalProjectReferences),
join(context.workspaceRoot, lockFileName),
].map(hashFile),
hashObject(options),
@ -239,6 +256,11 @@ function buildTscTargets(
context.workspaceRoot,
projectRoot
);
const externalProjectReferences = resolveShallowExternalProjectReferences(
tsConfig,
context.workspaceRoot,
projectRoot
);
const targetName = options.typecheck.targetName;
if (!targets[targetName]) {
let command = `tsc --build --emitDeclarationOnly --pretty --verbose`;
@ -246,11 +268,13 @@ function buildTscTargets(
tsConfig.options.noEmit ||
Object.values(internalProjectReferences).some(
(ref) => ref.options.noEmit
) ||
Object.values(externalProjectReferences).some(
(ref) => ref.options.noEmit
)
) {
// `--emitDeclarationOnly` and `--noEmit` are mutually exclusive, so
// we remove `--emitDeclarationOnly` if `--noEmit` is set.
command = `tsc --build --pretty --verbose`;
// `tsc --build` does not work with `noEmit: true`
command = `echo "The 'typecheck' target is disabled because one or more project references set 'noEmit: true' in their tsconfig. Remove this property to resolve this issue."`;
}
targets[targetName] = {
@ -607,6 +631,48 @@ function resolveInternalProjectReferences(
workspaceRoot: string,
projectRoot: string,
projectReferences: Record<string, ParsedCommandLine> = {}
): Record<string, ParsedCommandLine> {
walkProjectReferences(
tsConfig,
workspaceRoot,
projectRoot,
(configPath, config) => {
if (isExternalProjectReference(configPath, workspaceRoot, projectRoot)) {
return false;
} else {
projectReferences[configPath] = config;
}
}
);
return projectReferences;
}
function resolveShallowExternalProjectReferences(
tsConfig: ParsedCommandLine,
workspaceRoot: string,
projectRoot: string,
projectReferences: Record<string, ParsedCommandLine> = {}
): Record<string, ParsedCommandLine> {
walkProjectReferences(
tsConfig,
workspaceRoot,
projectRoot,
(configPath, config) => {
if (isExternalProjectReference(configPath, workspaceRoot, projectRoot)) {
projectReferences[configPath] = config;
}
return false;
}
);
return projectReferences;
}
function walkProjectReferences(
tsConfig: ParsedCommandLine,
workspaceRoot: string,
projectRoot: string,
visitor: (configPath: string, config: ParsedCommandLine) => void | false, // false stops recursion
projectReferences: Record<string, ParsedCommandLine> = {}
): Record<string, ParsedCommandLine> {
if (!tsConfig.projectReferences?.length) {
return projectReferences;
@ -624,22 +690,14 @@ function resolveInternalProjectReferences(
continue;
}
if (isExternalProjectReference(refConfigPath, workspaceRoot, projectRoot)) {
continue;
}
if (!refConfigPath.endsWith('.json')) {
refConfigPath = join(refConfigPath, 'tsconfig.json');
}
const refTsConfig = readCachedTsConfig(refConfigPath);
projectReferences[refConfigPath] = refTsConfig;
resolveInternalProjectReferences(
refTsConfig,
workspaceRoot,
projectRoot,
projectReferences
);
const result = visitor(refConfigPath, refTsConfig);
if (result !== false) {
walkProjectReferences(refTsConfig, workspaceRoot, projectRoot, visitor);
}
}
return projectReferences;

View File

@ -1,7 +1,6 @@
import {
detectPackageManager,
getPackageManagerVersion,
isWorkspacesEnabled,
output,
readJson,
type GeneratorCallback,
@ -10,6 +9,7 @@ import {
import { minimatch } from 'minimatch';
import { join } from 'node:path/posix';
import { getGlobPatternsFromPackageManagerWorkspaces } from 'nx/src/plugins/package-json';
import { PackageJson } from 'nx/src/utils/package-json';
import { lt } from 'semver';
export type ProjectPackageManagerWorkspaceState =
@ -37,7 +37,22 @@ export function getProjectPackageManagerWorkspaceState(
}
export function isUsingPackageManagerWorkspaces(tree: Tree): boolean {
return isWorkspacesEnabled(detectPackageManager(tree.root), tree.root);
return isWorkspacesEnabled(tree);
}
export function isWorkspacesEnabled(
tree: Tree
// packageManager: PackageManager = detectPackageManager(),
// root: string = workspaceRoot
): boolean {
const packageManager = detectPackageManager(tree.root);
if (packageManager === 'pnpm') {
return tree.exists('pnpm-workspace.yaml');
}
// yarn and npm both use the same 'workspaces' property in package.json
const packageJson = readJson<PackageJson>(tree, 'package.json');
return !!packageJson?.workspaces;
}
export function getProjectPackageManagerWorkspaceStateWarningTask(

View File

@ -1,12 +1,16 @@
import {
joinPathFragments,
offsetFromRoot,
output,
readJson,
readNxJson,
workspaceRoot,
type Tree,
updateJson,
workspaceRoot,
} from '@nx/devkit';
import { FsTree } from 'nx/src/generators/tree';
import { isUsingPackageManagerWorkspaces } from '../package-manager-workspaces';
import { relative } from 'node:path/posix';
export function isUsingTypeScriptPlugin(tree: Tree): boolean {
const nxJson = readNxJson(tree);
@ -96,3 +100,78 @@ export function assertNotUsingTsSolutionSetup(
process.exit(1);
}
export function updateTsconfigFiles(
tree: Tree,
projectRoot: string,
runtimeTsconfigFileName: string,
compilerOptions: Record<string, string | boolean | string[]>,
exclude: string[] = [],
rootDir = 'src'
) {
if (!isUsingTsSolutionSetup(tree)) return;
const offset = offsetFromRoot(projectRoot);
const tsconfig = `${projectRoot}/${runtimeTsconfigFileName}`;
const tsconfigSpec = `${projectRoot}/tsconfig.spec.json`;
const e2eRoot = `${projectRoot}-e2e`;
const tsconfigE2E = `${e2eRoot}/tsconfig.json`;
if (tree.exists(tsconfig)) {
updateJson(tree, tsconfig, (json) => {
json.extends = joinPathFragments(offset, 'tsconfig.base.json');
json.compilerOptions = {
...json.compilerOptions,
// Make sure d.ts files from typecheck does not conflict with bundlers.
// Other tooling like jest write to "out-tsc/jest" to we just default to "out-tsc/<project-name>".
outDir: joinPathFragments('out-tsc', projectRoot.split('/').at(-1)),
rootDir,
...compilerOptions,
};
const excludeSet: Set<string> = json.exclude
? new Set(['dist', ...json.exclude, ...exclude])
: new Set(exclude);
json.exclude = Array.from(excludeSet);
return json;
});
}
if (tree.exists(tsconfigSpec)) {
updateJson(tree, tsconfigSpec, (json) => {
json.extends = joinPathFragments(offset, 'tsconfig.base.json');
json.compilerOptions = {
...json.compilerOptions,
...compilerOptions,
};
const runtimePath = `./${runtimeTsconfigFileName}`;
json.references ??= [];
if (!json.references.some((x) => x.path === runtimePath))
json.references.push({ path: runtimePath });
return json;
});
}
if (tree.exists(tsconfigE2E)) {
// tsconfig.json for e2e projects need to have references array
updateJson(tree, tsconfigE2E, (json) => {
json.references ??= [];
const projectPath = relative(e2eRoot, projectRoot);
if (!json.references.some((x) => x.path === projectPath))
json.references.push({ path: projectPath });
return json;
});
}
if (tree.exists('tsconfig.json')) {
updateJson(tree, 'tsconfig.json', (json) => {
const projectPath = './' + projectRoot;
json.references ??= [];
if (!json.references.some((x) => x.path === projectPath))
json.references.push({ path: projectPath });
return json;
});
}
}

View File

@ -5,6 +5,8 @@ import {
readNxJson,
readProjectConfiguration,
Tree,
updateJson,
writeJson,
} from '@nx/devkit';
import { Schema } from './schema';
@ -174,23 +176,22 @@ describe('app', () => {
describe('--style scss', () => {
it('should generate scss styles', async () => {
const name = uniq();
await applicationGenerator(tree, {
directory: name,
directory: 'myapp',
style: 'scss',
});
expect(tree.exists(`${name}/src/app/page.module.scss`)).toBeTruthy();
expect(tree.exists(`${name}/src/app/global.css`)).toBeTruthy();
expect(tree.exists(`myapp/src/app/page.module.scss`)).toBeTruthy();
expect(tree.exists(`myapp/src/app/global.css`)).toBeTruthy();
const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8');
const indexContent = tree.read(`myapp/src/app/page.tsx`, 'utf-8');
expect(indexContent).toContain(`import styles from './page.module.scss'`);
expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8'))
expect(tree.read(`myapp/src/app/layout.tsx`, 'utf-8'))
.toMatchInlineSnapshot(`
"import './global.css';
export const metadata = {
title: 'Welcome to ${name}',
title: 'Welcome to myapp',
description: 'Generated by create-nx-workspace',
};
@ -212,23 +213,22 @@ describe('app', () => {
describe('--style less', () => {
it('should generate less styles', async () => {
const name = uniq();
await applicationGenerator(tree, {
directory: name,
directory: 'myapp',
style: 'less',
});
expect(tree.exists(`${name}/src/app/page.module.less`)).toBeTruthy();
expect(tree.exists(`${name}/src/app/global.less`)).toBeTruthy();
expect(tree.exists(`myapp/src/app/page.module.less`)).toBeTruthy();
expect(tree.exists(`myapp/src/app/global.less`)).toBeTruthy();
const indexContent = tree.read(`${name}/src/app/page.tsx`, 'utf-8');
const indexContent = tree.read(`myapp/src/app/page.tsx`, 'utf-8');
expect(indexContent).toContain(`import styles from './page.module.less'`);
expect(tree.read(`${name}/src/app/layout.tsx`, 'utf-8'))
expect(tree.read(`myapp/src/app/layout.tsx`, 'utf-8'))
.toMatchInlineSnapshot(`
"import './global.less';
export const metadata = {
title: 'Welcome to ${name}',
title: 'Welcome to myapp',
description: 'Generated by create-nx-workspace',
};
@ -616,10 +616,8 @@ describe('app', () => {
});
it('should add .eslintrc.json and dependencies', async () => {
const name = uniq();
await applicationGenerator(tree, {
directory: name,
directory: 'myapp',
style: 'css',
});
@ -631,7 +629,7 @@ describe('app', () => {
},
});
const eslintJson = readJson(tree, `${name}/.eslintrc.json`);
const eslintJson = readJson(tree, `myapp/.eslintrc.json`);
expect(eslintJson).toMatchInlineSnapshot(`
{
"extends": [
@ -655,7 +653,7 @@ describe('app', () => {
"rules": {
"@next/next/no-html-link-for-pages": [
"error",
"${name}/pages",
"myapp/pages",
],
},
},
@ -838,6 +836,172 @@ describe('app (legacy)', () => {
expect(projectConfiguration.targets.build).toBeDefined();
expect(projectConfiguration.targets.serve).toBeDefined();
});
describe('TS solution setup', () => {
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*'];
return json;
});
writeJson(tree, 'tsconfig.base.json', {
compilerOptions: {
composite: true,
declaration: true,
},
});
writeJson(tree, 'tsconfig.json', {
extends: './tsconfig.base.json',
files: [],
references: [],
});
});
it('should add project references when using TS solution', async () => {
await applicationGenerator(tree, {
...schema,
addPlugin: true,
directory: 'myapp',
name: 'myapp',
});
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
[
{
"path": "./myapp-e2e",
},
{
"path": "./myapp",
},
]
`);
expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"isolatedModules": true,
"jsx": "preserve",
"lib": [
"dom",
"dom.iterable",
"esnext",
],
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"outDir": "out-tsc/myapp",
"paths": {
"@/*": [
"./src/*",
],
},
"plugins": [
{
"name": "next",
},
],
"resolveJsonModule": true,
"rootDir": "src",
"strict": true,
"types": [
"jest",
"node",
],
},
"exclude": [
"dist",
"node_modules",
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
".next",
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs",
],
"extends": "../tsconfig.base.json",
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.js",
"src/**/*.jsx",
"../myapp/.next/types/**/*.ts",
"../dist/myapp/.next/types/**/*.ts",
"next-env.d.ts",
],
}
`);
expect(readJson(tree, 'myapp/tsconfig.spec.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "./out-tsc/jest",
"types": [
"jest",
"node",
],
},
"extends": "../tsconfig.base.json",
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts",
],
"references": [
{
"path": "./tsconfig.json",
},
],
}
`);
expect(readJson(tree, 'myapp-e2e/tsconfig.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"outDir": "dist",
"sourceMap": false,
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
"types": [
"cypress",
"node",
],
},
"exclude": [
"dist",
],
"extends": "../tsconfig.base.json",
"include": [
"**/*.ts",
"**/*.js",
"cypress.config.ts",
"**/*.cy.ts",
"**/*.cy.tsx",
"**/*.cy.js",
"**/*.cy.jsx",
"**/*.d.ts",
],
"references": [
{
"path": "../myapp",
},
],
}
`);
});
});
});
function uniq() {

View File

@ -7,7 +7,6 @@ import {
Tree,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { setupTailwindGenerator } from '@nx/react';
import {
testingLibraryReactVersion,
@ -31,6 +30,7 @@ import { updateCypressTsConfig } from './lib/update-cypress-tsconfig';
import { showPossibleWarnings } from './lib/show-possible-warnings';
import { tsLibVersion } from '../../utils/versions';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function applicationGenerator(host: Tree, schema: Schema) {
return await applicationGeneratorInternal(host, {
@ -40,8 +40,6 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
}
export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
assertNotUsingTsSolutionSetup(host, 'next', 'application');
const tasks: GeneratorCallback[] = [];
const options = await normalizeOptions(host, schema);
@ -51,6 +49,8 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
js: options.js,
skipPackageJson: options.skipPackageJson,
skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
});
tasks.push(jsInitTask);
@ -117,6 +117,21 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
);
}
updateTsconfigFiles(
host,
options.appProjectRoot,
'tsconfig.json',
{
jsx: 'preserve',
module: 'esnext',
moduleResolution: 'bundler',
},
options.linter === 'eslint'
? ['.next', 'eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
: ['.next'],
options.src ? 'src' : '.'
);
if (!options.skipFormat) {
await formatFiles(host);
}

View File

@ -3,16 +3,22 @@
"compilerOptions": {
"jsx": "preserve",
<% if (style === '@emotion/styled') { %>"jsxImportSource": "@emotion/react",<% } %>
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"plugins": [{ "name": "next" }]
"plugins": [{ "name": "next" }]<% if (isUsingTsSolutionSetup) { %>,
"paths": {
"@/*": [<% if (src) { %>"./src/*"<% } else { %>"./*"<% } %>]
}<% } %>
},
"include": [
"<%= rootPath %>**/*.ts",

View File

@ -1,10 +1,10 @@
import {
addProjectConfiguration,
ensurePackage,
getPackageManagerCommand,
joinPathFragments,
readNxJson,
Tree,
writeJson,
} from '@nx/devkit';
import { Linter } from '@nx/eslint';
@ -14,6 +14,7 @@ import { webStaticServeGenerator } from '@nx/web';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file';
import { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { getE2EWebServerInfo } from '@nx/devkit/src/generators/e2e-web-server-info-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function addE2e(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
@ -44,13 +45,31 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
});
}
if (isUsingTsSolutionSetup(host)) {
writeJson(
host,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
{
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName],
},
}
);
} else {
addProjectConfiguration(host, options.e2eProjectName, {
root: options.e2eProjectRoot,
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
tags: [],
implicitDependencies: [options.projectName],
});
}
const e2eTask = await configurationGenerator(host, {
...options,
@ -107,13 +126,32 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
if (isUsingTsSolutionSetup(host)) {
writeJson(
host,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
{
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName],
},
}
);
} else {
addProjectConfiguration(host, options.e2eProjectName, {
root: options.e2eProjectRoot,
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
tags: [],
implicitDependencies: [options.projectName],
});
}
const e2eTask = await configurationGenerator(host, {
rootProject: options.rootProject,
project: options.e2eProjectName,

View File

@ -22,6 +22,8 @@ export async function addLinting(
host: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
if (options.linter !== 'eslint') return () => {};
const tasks: GeneratorCallback[] = [];
tasks.push(

View File

@ -1,11 +1,17 @@
import { NormalizedSchema } from './normalize-options';
import {
addProjectConfiguration,
joinPathFragments,
ProjectConfiguration,
readNxJson,
Tree,
writeJson,
} from '@nx/devkit';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
import { nextVersion } from '../../../utils/versions';
import { reactDomVersion, reactVersion } from '@nx/react';
export function addProject(host: Tree, options: NormalizedSchema) {
const targets: Record<string, any> = {};
@ -66,7 +72,26 @@ export function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags,
};
if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: getImportPath(host, options.name),
version: '0.0.1',
private: true,
dependencies: {
next: nextVersion,
react: reactVersion,
'react-dom': reactDomVersion,
},
nx: {
name: options.name,
projectType: 'application',
sourceRoot: options.appProjectRoot,
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
});
} else {
addProjectConfiguration(host, options.projectName, {
...project,
});
}
}

View File

@ -16,6 +16,7 @@ import {
createAppJsx,
createStyleRules,
} from './create-application-files.helpers';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
const offsetFromRoot = _offsetFromRoot(options.appProjectRoot);
@ -30,14 +31,15 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
'.next/types/**/*.ts'
);
// scope tsconfig to the project directory so that it doesn't include other projects/libs
const rootPath = options.rootProject
const rootPath =
options.rootProject || isUsingTsSolutionSetup(host)
? options.src
? 'src/'
: options.appDir
? 'app/'
: 'pages/'
: '';
const templateVariables = {
...names(options.name),
...options,
@ -55,8 +57,8 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
appContent: createAppJsx(options.projectName),
styleContent: createStyleRules(),
pageStyleContent: `.page {}`,
stylesExt: options.style === 'less' ? options.style : 'css',
isUsingTsSolutionSetup: isUsingTsSolutionSetup(host),
};
const generatedAppFilePath = options.src

View File

@ -17,6 +17,9 @@ export interface Schema {
skipPackageJson?: boolean;
appDir?: boolean;
src?: boolean;
// Internal options
rootProject?: boolean;
addPlugin?: boolean;
useTsSolution?: boolean;
formatter?: 'prettier' | 'none';
}

View File

@ -69,8 +69,10 @@
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
"enum": ["eslint", "none"],
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"skipFormat": {
"description": "Skip formatting files.",
@ -82,7 +84,9 @@
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"default": "none",
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"e2eTestRunner": {
"type": "string",

View File

@ -1,4 +1,4 @@
import type { Tree } from '@nx/devkit';
import { joinPathFragments, Tree } from '@nx/devkit';
import {
updateJson,
generateFiles,
@ -11,6 +11,7 @@ import {
import { CustomServerSchema } from './schema';
import { join } from 'path';
import { configureForSwc } from '../../utils/add-swc-to-custom-server';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function customServerGenerator(
host: Tree,
@ -71,12 +72,18 @@ export async function customServerGenerator(
project.root
}`;
const offset = offsetFromRoot(project.root);
const isTsSolution = isUsingTsSolutionSetup(host);
generateFiles(host, join(__dirname, 'files'), project.root, {
...options,
hasPlugin,
projectPathFromDist,
offsetFromRoot: offsetFromRoot(project.root),
offsetFromRoot: offset,
projectRoot: project.root,
baseTsConfigPath: isTsSolution
? joinPathFragments(offset, 'tsconfig.base.json')
: './tsconfig.json',
tmpl: '',
});

View File

@ -1,7 +1,8 @@
{
"extends": "./tsconfig.json",
"extends": "<%= baseTsConfigPath %>",
"compilerOptions": {
"module": "commonjs",
"module": "nodenext",
"moduleResolution": "nodenext",
"noEmit": false,
"incremental": true,
<% if(hasPlugin && compiler === 'tsc') { %>

View File

@ -8,7 +8,6 @@ import {
createProjectGraphAsync,
} from '@nx/devkit';
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { reactDomVersion, reactVersion } from '@nx/react/src/utils/versions';
import { addGitIgnoreEntry } from '../../utils/add-gitignore-entry';
import { nextVersion, nxVersion } from '../../utils/versions';
@ -46,8 +45,6 @@ export async function nextInitGeneratorInternal(
host: Tree,
schema: InitSchema
) {
assertNotUsingTsSolutionSetup(host, 'next', 'init');
const nxJson = readNxJson(host);
const addPluginDefault =
process.env.NX_ADD_PLUGINS !== 'false' &&

View File

@ -9,13 +9,13 @@ import {
} from '@nx/devkit';
import { libraryGenerator as reactLibraryGenerator } from '@nx/react/src/generators/library/library';
import { addTsConfigPath, initGenerator as jsInitGenerator } from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { testingLibraryReactVersion } from '@nx/react/src/utils/versions';
import { nextInitGenerator } from '../init/init';
import { Schema } from './schema';
import { normalizeOptions } from './lib/normalize-options';
import { eslintConfigNextVersion, tsLibVersion } from '../../utils/versions';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function libraryGenerator(host: Tree, rawOptions: Schema) {
return await libraryGeneratorInternal(host, {
@ -25,8 +25,6 @@ export async function libraryGenerator(host: Tree, rawOptions: Schema) {
}
export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) {
assertNotUsingTsSolutionSetup(host, 'next', 'library');
const options = await normalizeOptions(host, rawOptions);
const tasks: GeneratorCallback[] = [];
@ -45,7 +43,7 @@ export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) {
const libTask = await reactLibraryGenerator(host, {
...options,
compiler: 'swc',
bundler: 'none',
skipFormat: true,
});
tasks.push(libTask);
@ -142,6 +140,20 @@ export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) {
}
);
updateTsconfigFiles(
host,
options.projectRoot,
'tsconfig.lib.json',
{
jsx: 'react-jsx',
module: 'esnext',
moduleResolution: 'bundler',
},
options.linter === 'eslint'
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
: undefined
);
if (!options.skipFormat) {
await formatFiles(host);
}

View File

@ -14,7 +14,9 @@ export interface Schema {
linter: Linter | LinterType;
component?: boolean;
publishable?: boolean;
/** @deprecated Use bundler instead. */
buildable?: boolean;
bundler?: 'none' | 'vite' | 'rollup';
importPath?: string;
js?: boolean;
globalCss?: boolean;

View File

@ -62,17 +62,29 @@
]
}
},
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
"enum": ["eslint", "none"],
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "vitest"
"default": "none",
"x-prompt": "What unit test runner should be used?",
"x-priority": "important"
},
"tags": {
"type": "string",
@ -105,7 +117,8 @@
"buildable": {
"type": "boolean",
"default": false,
"description": "Generate a buildable library."
"description": "Generate a buildable library that uses rollup to bundle.",
"x-deprecated": "Use the `bundler` option for greater control (none, vite, rollup)."
},
"importPath": {
"type": "string",

View File

@ -95,12 +95,17 @@ export async function configurationGeneratorInternal(
};
if (isTsSolutionSetup) {
// skip eslint from typechecking since it extends from root file that is outside rootDir
if (options.linter === 'eslint') {
tsconfig.exclude = ['dist', 'eslint.config.js'];
}
tsconfig.compilerOptions.outDir = 'dist';
tsconfig.compilerOptions.tsBuildInfoFile = 'dist/tsconfig.tsbuildinfo';
if (!options.rootProject) {
// add the project tsconfog to the workspace root tsconfig.json references
updateJson(tree, 'tsconfig.json', (json) => {
// add the project tsconfig to the workspace root tsconfig.json references
json.references ??= [];
json.references.push({ path: './' + projectConfig.root });
return json;
@ -130,6 +135,9 @@ export async function configurationGeneratorInternal(
name: importPath,
version: '0.0.1',
private: true,
nx: {
name: options.project,
},
};
writeJson(tree, packageJsonPath, packageJson);
}

View File

@ -5,6 +5,8 @@ import {
getProjects,
readJson,
readProjectConfiguration,
updateJson,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
@ -249,4 +251,152 @@ describe('app', () => {
expect(readJson(appTree, 'package.json')).toEqual(packageJsonBefore);
});
});
describe('TS solution setup', () => {
it('should add project references when using TS solution', async () => {
const tree = createTreeWithEmptyWorkspace();
tree.write('.gitignore', '');
updateJson(tree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*'];
return json;
});
writeJson(tree, 'tsconfig.base.json', {
compilerOptions: {
composite: true,
declaration: true,
},
});
writeJson(tree, 'tsconfig.json', {
extends: './tsconfig.base.json',
files: [],
references: [],
});
await reactNativeApplicationGenerator(tree, {
directory: 'my-app',
displayName: 'myApp',
tags: 'one,two',
linter: Linter.EsLint,
e2eTestRunner: 'none',
install: false,
unitTestRunner: 'jest',
bundler: 'vite',
addPlugin: true,
});
expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(`
[
{
"path": "./my-app",
},
]
`);
expect(readJson(tree, 'my-app/tsconfig.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"jsx": "react-native",
"lib": [
"dom",
"esnext",
],
"moduleResolution": "node",
"resolveJsonModule": true,
"skipLibCheck": true,
},
"extends": "../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json",
},
{
"path": "./tsconfig.spec.json",
},
],
}
`);
expect(readJson(tree, 'my-app/tsconfig.app.json')).toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": [
"dom",
],
"module": "esnext",
"moduleResolution": "bundler",
"noUnusedLocals": false,
"outDir": "out-tsc/my-app",
"rootDir": "src",
"types": [
"node",
],
},
"exclude": [
"dist",
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.spec.tsx",
"src/test-setup.ts",
"src/**/*.test.ts",
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs",
],
"extends": "../tsconfig.base.json",
"files": [
"../node_modules/@nx/react-native/typings/svg.d.ts",
],
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.js",
"src/**/*.jsx",
],
}
`);
expect(readJson(tree, 'my-app/tsconfig.spec.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": [
"dom",
],
"module": "esnext",
"moduleResolution": "bundler",
"noUnusedLocals": false,
"outDir": "./out-tsc/jest",
"types": [
"jest",
"node",
],
},
"extends": "../tsconfig.base.json",
"files": [
"src/test-setup.ts",
],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts",
],
"references": [
{
"path": "./tsconfig.app.json",
},
],
}
`);
});
});
});

View File

@ -9,7 +9,6 @@ import {
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest';
@ -26,6 +25,7 @@ import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { syncDeps } from '../../executors/sync-deps/sync-deps.impl';
import { PackageJson } from 'nx/src/utils/package-json';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function reactNativeApplicationGenerator(
host: Tree,
@ -41,16 +41,16 @@ export async function reactNativeApplicationGeneratorInternal(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'react-native', 'application');
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
});
tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema);
const initTask = await initGenerator(host, { ...options, skipFormat: true });
tasks.push(initTask);
@ -127,6 +127,22 @@ export async function reactNativeApplicationGeneratorInternal(
});
}
updateTsconfigFiles(
host,
options.appProjectRoot,
'tsconfig.app.json',
{
jsx: 'react-jsx',
module: 'esnext',
moduleResolution: 'bundler',
noUnusedLocals: false,
lib: ['dom'],
},
options.linter === 'eslint'
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
: undefined
);
if (!options.skipFormat) {
await formatFiles(host);
}

View File

@ -1,11 +1,15 @@
import {
addProjectConfiguration,
joinPathFragments,
ProjectConfiguration,
readNxJson,
TargetConfiguration,
Tree,
writeJson,
} from '@nx/devkit';
import { NormalizedSchema } from './normalize-options';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host);
@ -23,10 +27,25 @@ export function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags,
};
if (isUsingTsSolutionSetup(host)) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: getImportPath(host, options.name),
version: '0.0.1',
private: true,
nx: {
name: options.name,
projectType: 'application',
sourceRoot: `${options.appProjectRoot}/src`,
targets: hasPlugin ? {} : getTargets(options),
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
});
} else {
addProjectConfiguration(host, options.projectName, {
...project,
});
}
}
function getTargets(options: NormalizedSchema) {
const architect: { [key: string]: TargetConfiguration } = {};

View File

@ -16,6 +16,9 @@ export interface Schema {
bundler: 'webpack' | 'vite'; // default is webpack
install: boolean; // default is true
skipPackageJson?: boolean; //default is false
// Internal options
addPlugin?: boolean;
nxCloudToken?: string;
useTsSolution?: boolean;
formatter?: 'prettier' | 'none';
}

View File

@ -44,13 +44,15 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",
@ -71,7 +73,8 @@
"description": "Adds the specified e2e test runner.",
"type": "string",
"enum": ["playwright", "cypress", "detox", "none"],
"default": "playwright"
"default": "none",
"x-priority": "important"
},
"install": {
"type": "boolean",

View File

@ -9,7 +9,6 @@ import {
Tree,
} from '@nx/devkit';
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { createNodes } from '../../../plugins/plugin';
import {
nxVersion,
@ -31,8 +30,6 @@ export async function reactNativeInitGeneratorInternal(
host: Tree,
schema: Schema
) {
assertNotUsingTsSolutionSetup(host, 'react-native', 'init');
addGitIgnoreEntry(host);
const nxJson = readNxJson(host);

View File

@ -4,6 +4,7 @@ import {
ensureProjectName,
} from '@nx/devkit/src/generators/project-name-and-root-utils';
import { Schema } from '../schema';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export interface NormalizedSchema extends Schema {
name: string;
@ -13,6 +14,7 @@ export interface NormalizedSchema extends Schema {
parsedTags: string[];
appMain?: string;
appSourceRoot?: string;
isUsingTsSolutionConfig: boolean;
}
export async function normalizeOptions(
@ -50,6 +52,7 @@ export async function normalizeOptions(
projectRoot,
parsedTags,
importPath,
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host),
};
return normalized;

View File

@ -227,6 +227,8 @@ describe('lib', () => {
"compilerOptions": {
"outDir": "../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"jsx": "react-jsx",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],

View File

@ -4,6 +4,7 @@ import {
formatFiles,
generateFiles,
GeneratorCallback,
installPackagesTask,
joinPathFragments,
names,
offsetFromRoot,
@ -13,6 +14,7 @@ import {
Tree,
updateJson,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import {
@ -32,7 +34,11 @@ import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import {
isUsingTsSolutionSetup,
updateTsconfigFiles,
} from '@nx/js/src/utils/typescript/ts-solution-setup';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export async function reactNativeLibraryGenerator(
host: Tree,
@ -48,7 +54,13 @@ export async function reactNativeLibraryGeneratorInternal(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'react-native', 'library');
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema);
if (options.publishable === true && !schema.importPath) {
@ -57,13 +69,6 @@ export async function reactNativeLibraryGeneratorInternal(
);
}
const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const initTask = await init(host, { ...options, skipFormat: true });
tasks.push(initTask);
@ -111,11 +116,29 @@ export async function reactNativeLibraryGeneratorInternal(
),
]);
}
updateTsconfigFiles(
host,
options.projectRoot,
'tsconfig.lib.json',
{
jsx: 'react-jsx',
module: 'esnext',
moduleResolution: 'bundler',
},
options.linter === 'eslint'
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
: undefined
);
if (!options.skipFormat) {
await formatFiles(host);
}
// Always run install to link packages.
if (options.isUsingTsSolutionConfig) {
tasks.push(() => installPackagesTask(host));
}
tasks.push(() => {
logShowProjectCommand(options.name);
});
@ -135,7 +158,27 @@ async function addProject(
targets: {},
};
if (options.isUsingTsSolutionConfig) {
const sourceEntry = !options.buildable
? options.js
? './src/index.js'
: './src/index.ts'
: undefined;
writeJson(host, joinPathFragments(options.projectRoot, 'package.json'), {
name: getImportPath(host, options.name),
version: '0.0.1',
main: sourceEntry,
types: sourceEntry,
nx: {
name: options.name,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
});
} else {
addProjectConfiguration(host, options.name, project);
}
if (!options.publishable && !options.buildable) {
return () => {};

View File

@ -32,13 +32,16 @@
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
"default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"default": "none",
"x-priority": "important"
},
"tags": {
"type": "string",

View File

@ -12,7 +12,11 @@ import {
} from '@nx/devkit';
import { hasWebpackPlugin } from '@nx/react/src/utils/has-webpack-plugin';
import { nxVersion, reactNativeWebVersion } from '../../utils/versions';
import {
nxVersion,
reactNativeWebVersion,
typesReactDomVersion,
} from '../../utils/versions';
import { NormalizedSchema, normalizeSchema } from './lib/normalize-schema';
import {
createBuildTarget,
@ -77,6 +81,18 @@ export async function webConfigurationGenerator(
);
}
if (!options.skipPackageJson) {
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
'@types/react-dom': typesReactDomVersion,
}
)
);
}
if (!options.skipFormat) {
await formatFiles(tree);
}
@ -103,6 +119,7 @@ async function addBundlerConfiguration(
project: normalizedSchema.project,
newProject: true,
includeVitest: false,
projectType: 'application',
compiler: 'babel',
skipFormat: true,
});

View File

@ -14,6 +14,7 @@ export const reactVersion = '18.2.0';
export const reactDomVersion = '18.2.0';
export const reactTestRendererVersion = '18.2.0';
export const typesReactVersion = '~18.2.45';
export const typesReactDomVersion = '18.3.0';
export const testingLibraryReactNativeVersion = '~12.5.0';
export const testingLibraryJestNativeVersion = '~5.4.3';

View File

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app --minimal should create default application without Nx welcome component 1`] = `
"// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
"// Uncomment this line to use CSS modules
// import styles from './app.module.css';
export function App() {
return (
@ -239,8 +239,8 @@ export default defineConfig({
`;
exports[`app not nested should generate files 1`] = `
"// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
"// Uncomment this line to use CSS modules
// import styles from './app.module.css';
import NxWelcome from './nx-welcome';
export function App() {
@ -343,8 +343,8 @@ module.exports = {
`;
exports[`app should create Nx specific template 1`] = `
"// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
"// Uncomment this line to use CSS modules
// import styles from './app.module.css';
import NxWelcome from "./nx-welcome";
export function App() {

View File

@ -7,7 +7,9 @@ import {
readJson,
readNxJson,
Tree,
updateJson,
updateNxJson,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
@ -276,7 +278,6 @@ describe('app', () => {
expect(tsconfigApp.compilerOptions.outDir).toEqual('../dist/out-tsc');
expect(tsconfigApp.extends).toEqual('./tsconfig.json');
expect(tsconfigApp.exclude).toEqual([
'jest.config.ts',
'src/**/*.spec.ts',
'src/**/*.test.ts',
'src/**/*.spec.tsx',
@ -285,6 +286,7 @@ describe('app', () => {
'src/**/*.test.js',
'src/**/*.spec.jsx',
'src/**/*.test.jsx',
'jest.config.ts',
]);
const eslintJson = readJson(appTree, 'my-app/.eslintrc.json');
@ -414,7 +416,6 @@ describe('app', () => {
path: 'my-dir/my-app/tsconfig.app.json',
lookupFn: (json) => json.exclude,
expectedValue: [
'jest.config.ts',
'src/**/*.spec.ts',
'src/**/*.test.ts',
'src/**/*.spec.tsx',
@ -423,6 +424,7 @@ describe('app', () => {
'src/**/*.test.js',
'src/**/*.spec.jsx',
'src/**/*.test.jsx',
'jest.config.ts',
],
},
{
@ -1241,4 +1243,180 @@ describe('app', () => {
}
`);
});
describe('TS solution setup', () => {
beforeEach(() => {
appTree = createTreeWithEmptyWorkspace();
updateJson(appTree, 'package.json', (json) => {
json.workspaces = ['packages/*', 'apps/*'];
return json;
});
writeJson(appTree, 'tsconfig.base.json', {
compilerOptions: {
composite: true,
declaration: true,
},
});
writeJson(appTree, 'tsconfig.json', {
extends: './tsconfig.base.json',
files: [],
references: [],
});
});
it('should add project references when using TS solution', async () => {
await applicationGenerator(appTree, {
directory: 'myapp',
addPlugin: true,
linter: Linter.EsLint,
style: 'none',
bundler: 'vite',
unitTestRunner: 'vitest',
e2eTestRunner: 'playwright',
});
expect(readJson(appTree, 'tsconfig.json').references)
.toMatchInlineSnapshot(`
[
{
"path": "./myapp-e2e",
},
{
"path": "./myapp",
},
]
`);
expect(readJson(appTree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(`
{
"extends": "../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json",
},
{
"path": "./tsconfig.spec.json",
},
],
}
`);
expect(readJson(appTree, 'myapp/tsconfig.app.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "react-jsx",
"lib": [
"dom",
],
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "out-tsc/myapp",
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client",
],
},
"exclude": [
"dist",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
],
"extends": "../tsconfig.base.json",
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx",
],
}
`);
expect(readJson(appTree, 'myapp/tsconfig.spec.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
],
},
"extends": "../tsconfig.base.json",
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts",
],
"references": [
{
"path": "./tsconfig.app.json",
},
],
}
`);
expect(readJson(appTree, 'myapp-e2e/tsconfig.json'))
.toMatchInlineSnapshot(`
{
"compilerOptions": {
"allowJs": true,
"outDir": "dist",
"sourceMap": false,
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo",
},
"exclude": [
"dist",
"eslint.config.js",
],
"extends": "../tsconfig.base.json",
"include": [
"**/*.ts",
"**/*.js",
"playwright.config.ts",
"src/**/*.spec.ts",
"src/**/*.spec.js",
"src/**/*.test.ts",
"src/**/*.test.js",
"src/**/*.d.ts",
],
"references": [
{
"path": "../myapp",
},
],
}
`);
});
});
});

View File

@ -21,14 +21,9 @@ import {
Tree,
updateNxJson,
} from '@nx/devkit';
import reactInitGenerator from '../init/init';
import { Linter, lintProjectGenerator } from '@nx/eslint';
import {
babelLoaderVersion,
nxRspackVersion,
nxVersion,
} from '../../utils/versions';
import { babelLoaderVersion, nxVersion } from '../../utils/versions';
import { maybeJs } from '../../utils/maybe-js';
import { installCommonDependencies } from './lib/install-common-dependencies';
import { extractTsConfigBase } from '../../utils/create-ts-config';
@ -46,7 +41,7 @@ import { initGenerator as jsInitGenerator } from '@nx/js';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = [];
@ -114,20 +109,20 @@ export async function applicationGeneratorInternal(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'react', 'application');
const tasks = [];
const options = await normalizeOptions(host, schema);
showPossibleWarnings(host, options);
const jsInitTask = await jsInitGenerator(host, {
...schema,
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
});
tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema);
showPossibleWarnings(host, options);
const initTask = await reactInitGenerator(host, {
...options,
skipFormat: true,
@ -165,10 +160,7 @@ export async function applicationGeneratorInternal(
tasks.push(ensureDependencies(host, { uiFramework: 'react' }));
}
} else if (options.bundler === 'rspack') {
const { rspackInitGenerator } = ensurePackage(
'@nx/rspack',
nxRspackVersion
);
const { rspackInitGenerator } = ensurePackage('@nx/rspack', nxVersion);
const rspackInitTask = await rspackInitGenerator(host, {
...options,
addPlugin: false,
@ -213,6 +205,7 @@ export async function applicationGeneratorInternal(
compiler: options.compiler,
skipFormat: true,
addPlugin: options.addPlugin,
projectType: 'application',
});
tasks.push(viteTask);
createOrEditViteConfig(
@ -236,6 +229,26 @@ export async function applicationGeneratorInternal(
},
false
);
} else if (options.bundler === 'rspack') {
const { configurationGenerator } = ensurePackage('@nx/rspack', nxVersion);
const rspackTask = await configurationGenerator(host, {
project: options.projectName,
main: joinPathFragments(
options.appProjectRoot,
maybeJs(
{
js: options.js,
useJsx: true,
},
`src/main.tsx`
)
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
target: 'web',
newProject: true,
framework: 'react',
});
tasks.push(rspackTask);
}
if (options.bundler !== 'vite' && options.unitTestRunner === 'vitest') {
@ -348,6 +361,12 @@ export async function applicationGeneratorInternal(
);
}
updateTsconfigFiles(host, options.appProjectRoot, 'tsconfig.app.json', {
jsx: 'react-jsx',
module: 'esnext',
moduleResolution: 'bundler',
});
if (!options.skipFormat) {
await formatFiles(host);
}

View File

@ -1,14 +1,31 @@
{
<%_ if (isUsingTsSolutionSetup) { _%>{
"extends": "<%= offsetFromRoot%>tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"jsx": "react-jsx",
"lib": ["dom"],
"types": [
"node",
<%_ if (style === 'styled-jsx') { _%>"@nx/react/typings/styled-jsx.d.ts",<%_ } _%>
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}<% } else { %>{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"types": [
"node",
<%_ if (style === 'styled-jsx') { %>"@nx/react/typings/styled-jsx.d.ts",<% } _%>
<%_ if (style === 'styled-jsx') { _%>"@nx/react/typings/styled-jsx.d.ts",<%_ } _%>
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": ["jest.config.ts","src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
<% } %>

View File

@ -1,4 +1,20 @@
{
<%_ if (isUsingTsSolutionSetup) { _%>{
"extends": "<%= offsetFromRoot%>tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"jsx": "react-jsx",
"lib": ["dom"],
"types": [
"node",
<%_ if (style === 'styled-jsx') { _%>"@nx/react/typings/styled-jsx.d.ts",<%_ } _%>
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}<% } else { %>{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
@ -12,3 +28,4 @@
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
<% } %>

View File

@ -1,4 +1,20 @@
{
<%_ if (isUsingTsSolutionSetup) { _%>{
"extends": "<%= offsetFromRoot%>tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"jsx": "react-jsx",
"lib": ["dom"],
"types": [
"node",
<%_ if (style === 'styled-jsx') { _%>"@nx/react/typings/styled-jsx.d.ts",<%_ } _%>
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts"
]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}<% } else { %>{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
@ -9,6 +25,7 @@
"@nx/react/typings/image.d.ts"
]
},
"exclude": ["jest.config.ts","src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx", "src/**/*.spec.js", "src/**/*.test.js", "src/**/*.spec.jsx", "src/**/*.test.jsx"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
}
<% } %>

View File

@ -1,8 +1,8 @@
<% if (classComponent) { %>
import { Component } from 'react';
<%_ } _%>
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './<%= fileName %>.module.<%= style %>';
// Uncomment this line to use CSS modules
// import styles from './<%= fileName %>.module.<%= style %>';
<%_ if (!minimal) { _%>
import NxWelcome from "./nx-welcome";
<%_ } _%>

View File

@ -1,10 +1,12 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addProjectConfiguration,
ensurePackage,
GeneratorCallback,
getPackageManagerCommand,
joinPathFragments,
readNxJson,
Tree,
writeJson,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
@ -81,6 +83,22 @@ export async function addE2e(
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
if (options.isUsingTsSolutionConfig) {
writeJson(
tree,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
{
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName],
},
}
);
} else {
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
@ -89,6 +107,7 @@ export async function addE2e(
implicitDependencies: [options.projectName],
tags: [],
});
}
const e2eTask = await configurationGenerator(tree, {
...options,
@ -157,6 +176,22 @@ export async function addE2e(
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
if (options.isUsingTsSolutionConfig) {
writeJson(
tree,
joinPathFragments(options.e2eProjectRoot, 'package.json'),
{
name: options.e2eProjectName,
version: '0.0.1',
private: true,
nx: {
projectType: 'application',
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
implicitDependencies: [options.projectName],
},
}
);
} else {
addProjectConfiguration(tree, options.e2eProjectName, {
projectType: 'application',
root: options.e2eProjectRoot,
@ -164,6 +199,8 @@ export async function addE2e(
targets: {},
implicitDependencies: [options.projectName],
});
}
const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,

View File

@ -23,5 +23,6 @@ export async function addJest(
setupFile: 'none',
compiler: options.compiler,
skipFormat: true,
runtimeTsconfigFileName: 'tsconfig.app.json',
});
}

View File

@ -5,10 +5,12 @@ import {
ProjectConfiguration,
TargetConfiguration,
Tree,
writeJson,
} from '@nx/devkit';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { maybeJs } from '../../../utils/maybe-js';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function addProject(host: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration = {
@ -36,10 +38,26 @@ export function addProject(host: Tree, options: NormalizedSchema) {
};
}
if (options.isUsingTsSolutionConfig) {
writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
name: getImportPath(host, options.name),
version: '0.0.1',
private: true,
nx: {
name: options.name,
projectType: 'application',
sourceRoot: `${options.appProjectRoot}/src`,
tags: options.parsedTags?.length ? options.parsedTags : undefined,
},
});
}
if (!options.isUsingTsSolutionConfig || options.alwaysGenerateProjectJson) {
addProjectConfiguration(host, options.projectName, {
...project,
});
}
}
function createRspackBuildTarget(
options: NormalizedSchema

View File

@ -18,10 +18,11 @@ import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { NormalizedSchema } from '../schema';
import { getAppTests } from './get-app-tests';
import {
getNxCloudAppOnBoardingUrl,
createNxCloudOnboardingURLForWelcomeApp,
getNxCloudAppOnBoardingUrl,
} from 'nx/src/nx-cloud/utilities/onboarding';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function createApplicationFiles(
host: Tree,
@ -67,6 +68,7 @@ export async function createApplicationFiles(
inSourceVitestTests: getInSourceVitestTestsTemplate(appTests),
style: options.style === 'tailwind' ? 'css' : options.style,
hasStyleFile,
isUsingTsSolutionSetup: isUsingTsSolutionSetup(host),
};
if (options.bundler === 'vite') {

View File

@ -6,6 +6,7 @@ import {
import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema, Schema } from '../schema';
import { findFreePort } from './find-free-port';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
export function normalizeDirectory(options: Schema) {
options.directory = options.directory?.replace(/\\{1,2}/g, '/');
@ -67,6 +68,7 @@ export async function normalizeOptions<T extends Schema = Schema>(
fileName,
styledModule,
hasStyles: options.style !== 'none',
isUsingTsSolutionConfig: isUsingTsSolutionSetup(host),
} as NormalizedSchema;
normalized.routing = normalized.routing ?? false;

Some files were not shown because too many files have changed in this diff Show More