diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index b6d88f73b3da7..bb2e303ad511a 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -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 diff --git a/docs/generated/packages/expo/generators/application.json b/docs/generated/packages/expo/generators/application.json index 7eb94e7915e40..389e3f0c5e7b1 100644 --- a/docs/generated/packages/expo/generators/application.json +++ b/docs/generated/packages/expo/generators/application.json @@ -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 `/project.json` rather than including it inside `workspace.json`.", diff --git a/docs/generated/packages/expo/generators/library.json b/docs/generated/packages/expo/generators/library.json index ce7f139f379e4..ef35d1e653b73 100644 --- a/docs/generated/packages/expo/generators/library.json +++ b/docs/generated/packages/expo/generators/library.json @@ -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", diff --git a/docs/generated/packages/next/generators/application.json b/docs/generated/packages/next/generators/application.json index f78caf9f89447..fbba2beb03cd8 100644 --- a/docs/generated/packages/next/generators/application.json +++ b/docs/generated/packages/next/generators/application.json @@ -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", diff --git a/docs/generated/packages/next/generators/library.json b/docs/generated/packages/next/generators/library.json index 19a4977ccd637..d101fa4219834 100644 --- a/docs/generated/packages/next/generators/library.json +++ b/docs/generated/packages/next/generators/library.json @@ -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", diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index b6d88f73b3da7..bb2e303ad511a 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -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 diff --git a/docs/generated/packages/react-native/generators/application.json b/docs/generated/packages/react-native/generators/application.json index a151285fec4fc..4100814e2fc7c 100644 --- a/docs/generated/packages/react-native/generators/application.json +++ b/docs/generated/packages/react-native/generators/application.json @@ -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", diff --git a/docs/generated/packages/react-native/generators/library.json b/docs/generated/packages/react-native/generators/library.json index 4123a73aa224b..919ddf8b68879 100644 --- a/docs/generated/packages/react-native/generators/library.json +++ b/docs/generated/packages/react-native/generators/library.json @@ -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", diff --git a/docs/generated/packages/react/generators/application.json b/docs/generated/packages/react/generators/application.json index 4ecfc1f541a6f..2a4962bd94a83 100644 --- a/docs/generated/packages/react/generators/application.json +++ b/docs/generated/packages/react/generators/application.json @@ -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", diff --git a/docs/generated/packages/react/generators/library.json b/docs/generated/packages/react/generators/library.json index 1366c05768ddc..fa5b8dc3b5ad8 100644 --- a/docs/generated/packages/react/generators/library.json +++ b/docs/generated/packages/react/generators/library.json @@ -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"], diff --git a/docs/generated/packages/remix/generators/application.json b/docs/generated/packages/remix/generators/application.json index 2ddb29ae33ffe..773e6e4b840a9 100644 --- a/docs/generated/packages/remix/generators/application.json +++ b/docs/generated/packages/remix/generators/application.json @@ -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?" }, diff --git a/docs/generated/packages/remix/generators/library.json b/docs/generated/packages/remix/generators/library.json index f4400b418cb26..0844b41c46c82 100644 --- a/docs/generated/packages/remix/generators/library.json +++ b/docs/generated/packages/remix/generators/library.json @@ -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"], diff --git a/docs/generated/packages/rspack/executors/rspack.json b/docs/generated/packages/rspack/executors/rspack.json index 21d9dec4ae21b..3164e778891c1 100644 --- a/docs/generated/packages/rspack/executors/rspack.json +++ b/docs/generated/packages/rspack/executors/rspack.json @@ -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.", diff --git a/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index 966acc7342925..8ea49dbecb671 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -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, diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index b6dafd5b72c1f..3ebfe23c9410b 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -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"], diff --git a/e2e/eslint/src/linter-legacy.test.ts b/e2e/eslint/src/linter-legacy.test.ts index 0e33e0b910740..b561d4419fbb8 100644 --- a/e2e/eslint/src/linter-legacy.test.ts +++ b/e2e/eslint/src/linter-legacy.test.ts @@ -27,10 +27,13 @@ describe('Linter (legacy)', () => { newProject({ packages: ['@nx/react', '@nx/js', '@nx/eslint'], }); - runCLI(`generate @nx/react:app apps/${myapp} --tags=validtag`, { - env: { NX_ADD_PLUGINS: 'false' }, - }); - runCLI(`generate @nx/js:lib apps/${mylib}`, { + runCLI( + `generate @nx/react:app apps/${myapp} --tags=validtag --linter=eslint`, + { + env: { NX_ADD_PLUGINS: 'false' }, + } + ); + 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' }, }); diff --git a/e2e/eslint/src/linter.test.ts b/e2e/eslint/src/linter.test.ts index 49859e1371f5e..31a4c9a14dfc6 100644 --- a/e2e/eslint/src/linter.test.ts +++ b/e2e/eslint/src/linter.test.ts @@ -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`); diff --git a/e2e/expo/src/expo-legacy.test.ts b/e2e/expo/src/expo-legacy.test.ts index 82f852de687a3..39e2b5cea5bcb 100644 --- a/e2e/expo/src/expo-legacy.test.ts +++ b/e2e/expo/src/expo-legacy.test.ts @@ -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 }); diff --git a/e2e/expo/src/expo.test.ts b/e2e/expo/src/expo.test.ts index 3e5dfc7a7d638..c03e1b1fc2a51 100644 --- a/e2e/expo/src/expo.test.ts +++ b/e2e/expo/src/expo.test.ts @@ -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`, diff --git a/e2e/jest/src/jest-root.test.ts b/e2e/jest/src/jest-root.test.ts index 562ba69b83db9..90b4c24dfa29e 100644 --- a/e2e/jest/src/jest-root.test.ts +++ b/e2e/jest/src/jest-root.test.ts @@ -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(); diff --git a/e2e/next/src/next-styles.test.ts b/e2e/next/src/next-styles.test.ts index 219557036a1cd..b8d58707864d8 100644 --- a/e2e/next/src/next-styles.test.ts +++ b/e2e/next/src/next-styles.test.ts @@ -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, { diff --git a/e2e/next/src/next.test.ts b/e2e/next/src/next.test.ts index 5cc90aeaa6a85..fcf104e28b9f1 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next/src/next.test.ts @@ -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`; diff --git a/e2e/nx/src/workspace-legacy.test.ts b/e2e/nx/src/workspace-legacy.test.ts index ae7c96ec6f5f6..f3283e7fcf6e2 100644 --- a/e2e/nx/src/workspace-legacy.test.ts +++ b/e2e/nx/src/workspace-legacy.test.ts @@ -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', diff --git a/e2e/nx/src/workspace.test.ts b/e2e/nx/src/workspace.test.ts index 14f853054d1cc..6c6b1fb85e08c 100644 --- a/e2e/nx/src/workspace.test.ts +++ b/e2e/nx/src/workspace.test.ts @@ -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'); diff --git a/e2e/react-native/src/react-native-legacy.test.ts b/e2e/react-native/src/react-native-legacy.test.ts index 4b5186a01a139..4032caf85f522 100644 --- a/e2e/react-native/src/react-native-legacy.test.ts +++ b/e2e/react-native/src/react-native-legacy.test.ts @@ -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()) { diff --git a/e2e/react-native/src/react-native.test.ts b/e2e/react-native/src/react-native.test.ts index 28cac4386c3b1..544c01d279ef2 100644 --- a/e2e/react-native/src/react-native.test.ts +++ b/e2e/react-native/src/react-native.test.ts @@ -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()) { diff --git a/e2e/react/src/react-vite.test.ts b/e2e/react/src/react-vite.test.ts index 29193e84965a1..ec3d4770dc896 100644 --- a/e2e/react/src/react-vite.test.ts +++ b/e2e/react/src/react-vite.test.ts @@ -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}`); diff --git a/e2e/react/src/react.test.ts b/e2e/react/src/react.test.ts index 08120d47774e2..531703008168a 100644 --- a/e2e/react/src/react.test.ts +++ b/e2e/react/src/react.test.ts @@ -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 = ``; 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 diff --git a/e2e/remix/src/nx-remix.test.ts b/e2e/remix/src/nx-remix.test.ts index d96addeb6f503..4066485a9788b 100644 --- a/e2e/remix/src/nx-remix.test.ts +++ b/e2e/remix/src/nx-remix.test.ts @@ -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); diff --git a/e2e/rspack/tests/rspack.spec.ts b/e2e/rspack/tests/rspack.spec.ts index c9e0d1be30677..732728605d35e 100644 --- a/e2e/rspack/tests/rspack.spec.ts +++ b/e2e/rspack/tests/rspack.spec.ts @@ -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); }); diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 25bf031ba8554..05406751870df 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -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}`; } diff --git a/e2e/vite/src/vite-legacy.test.ts b/e2e/vite/src/vite-legacy.test.ts index 33841fa4bdca3..cc0117dd520ec 100644 --- a/e2e/vite/src/vite-legacy.test.ts +++ b/e2e/vite/src/vite-legacy.test.ts @@ -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 diff --git a/e2e/webpack/src/webpack.legacy.test.ts b/e2e/webpack/src/webpack.legacy.test.ts index f1e5b9b63701c..d50ca3a11b5c9 100644 --- a/e2e/webpack/src/webpack.legacy.test.ts +++ b/e2e/webpack/src/webpack.legacy.test.ts @@ -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`, diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 59911cdbf2a14..623e42a5d7ede 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -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 = 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,6 +452,52 @@ async function determinePresetOptions( } } +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', + message: `Would you like to use Prettier for code formatting?`, + type: 'autocomplete', + choices: [ + { + name: 'Yes', + }, + { + name: 'No', + }, + ], + initial: 1, + 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 ): Promise> { @@ -448,26 +506,9 @@ async function determineNoneOptions( process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_TS_PLUGIN !== 'false' ) { - const reply = await enquirer.prompt<{ prettier: 'Yes' | 'No' }>([ - { - name: 'prettier', - message: `Would you like to use Prettier for code formatting?`, - type: 'autocomplete', - choices: [ - { - name: 'Yes', - }, - { - name: 'No', - }, - ], - initial: 1, - skip: !parsedArgs.interactive || isCI(), - }, - ]); 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, }; } diff --git a/packages/create-nx-workspace/src/create-workspace.ts b/packages/create-nx-workspace/src/create-workspace.ts index 3d6cbf1d48192..728238309a4b8 100644 --- a/packages/create-nx-workspace/src/create-workspace.ts +++ b/packages/create-nx-workspace/src/create-workspace.ts @@ -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( preset: string, @@ -30,12 +31,14 @@ export async function createWorkspace( 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( 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/**']; + } +} diff --git a/packages/cypress/src/generators/base-setup/files/tsconfig/ts-solution/__directory__/tsconfig.json__ext__ b/packages/cypress/src/generators/base-setup/files/tsconfig/ts-solution/__directory__/tsconfig.json__ext__ index 871e69aadc0b9..88dc50152e31c 100644 --- a/packages/cypress/src/generators/base-setup/files/tsconfig/ts-solution/__directory__/tsconfig.json__ext__ +++ b/packages/cypress/src/generators/base-setup/files/tsconfig/ts-solution/__directory__/tsconfig.json__ext__ @@ -16,5 +16,6 @@ "<%= offsetFromProjectRoot %>**/*.cy.js", <%_ if (jsx) { _%>"<%= offsetFromProjectRoot %>**/*.cy.jsx",<%_ } _%> "<%= offsetFromProjectRoot %>**/*.d.ts" - ] + ], + "exclude": ["dist"<% if (linter === 'eslint') { %>, "eslint.config.js"<% } %>] } diff --git a/packages/cypress/src/generators/configuration/configuration.ts b/packages/cypress/src/generators/configuration/configuration.ts index 096296f4e2944..3671a480b416a 100644 --- a/packages/cypress/src/generators/configuration/configuration.ts +++ b/packages/cypress/src/generators/configuration/configuration.ts @@ -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,':[: 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); } diff --git a/packages/detox/src/generators/application/application.ts b/packages/detox/src/generators/application/application.ts index 3cd37e4d9998d..9b42fdf537bf0 100644 --- a/packages/detox/src/generators/application/application.ts +++ b/packages/detox/src/generators/application/application.ts @@ -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; diff --git a/packages/detox/src/generators/application/lib/add-project.ts b/packages/detox/src/generators/application/lib/add-project.ts index c3f44f57b785a..ebbdf53a98977 100644 --- a/packages/detox/src/generators/application/lib/add-project.ts +++ b/packages/detox/src/generators/application/lib/add-project.ts @@ -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,14 +23,28 @@ export function addProject(host: Tree, options: NormalizedSchema) { : p.plugin === '@nx/detox/plugin' ); - addProjectConfiguration(host, options.e2eProjectName, { - root: options.e2eProjectRoot, - sourceRoot: `${options.e2eProjectRoot}/src`, - projectType: 'application', - targets: hasPlugin ? {} : getTargets(options), - tags: [], - implicitDependencies: [options.appProject], - }); + 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`, + projectType: 'application', + targets: hasPlugin ? {} : getTargets(options), + tags: [], + implicitDependencies: [options.appProject], + }); + } } function getTargets(options: NormalizedSchema) { diff --git a/packages/detox/src/generators/init/init.ts b/packages/detox/src/generators/init/init.ts index a66360be9f4f2..543ea76ed86f9 100644 --- a/packages/detox/src/generators/init/init.ts +++ b/packages/detox/src/generators/init/init.ts @@ -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); diff --git a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts index 6b99850a039a4..c1f85c66d3e1a 100644 --- a/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts +++ b/packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts @@ -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) => { diff --git a/packages/expo/src/generators/application/application.spec.ts b/packages/expo/src/generators/application/application.spec.ts index f51d0690f7c2c..3753b0a235358 100644 --- a/packages/expo/src/generators/application/application.spec.ts +++ b/packages/expo/src/generators/application/application.spec.ts @@ -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", + }, + ], + } + `); + }); + }); }); diff --git a/packages/expo/src/generators/application/application.ts b/packages/expo/src/generators/application/application.ts index 058dc24445f30..4c971cbc324b1 100644 --- a/packages/expo/src/generators/application/application.ts +++ b/packages/expo/src/generators/application/application.ts @@ -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 { - 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); } diff --git a/packages/expo/src/generators/application/lib/add-e2e.ts b/packages/expo/src/generators/application/lib/add-e2e.ts index 67d16a84e15c5..4734949259b30 100644 --- a/packages/expo/src/generators/application/lib/add-e2e.ts +++ b/packages/expo/src/generators/application/lib/add-e2e.ts @@ -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,14 +42,31 @@ export async function addE2e( typeof import('@nx/cypress') >('@nx/cypress', nxVersion); - addProjectConfiguration(tree, options.e2eProjectName, { - projectType: 'application', - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - implicitDependencies: [options.projectName], - tags: [], - }); + 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, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + implicitDependencies: [options.projectName], + tags: [], + }); + } const e2eTask = await configurationGenerator(tree, { ...options, @@ -106,13 +125,30 @@ export async function addE2e( const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') >('@nx/playwright', nxVersion); - addProjectConfiguration(tree, options.e2eProjectName, { - projectType: 'application', - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - implicitDependencies: [options.projectName], - }); + 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, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + implicitDependencies: [options.projectName], + }); + } const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, diff --git a/packages/expo/src/generators/application/lib/add-project.ts b/packages/expo/src/generators/application/lib/add-project.ts index 1f79ea08c95c0..6067f11810d6e 100644 --- a/packages/expo/src/generators/application/lib/add-project.ts +++ b/packages/expo/src/generators/application/lib/add-project.ts @@ -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,12 +30,28 @@ export function addProject(host: Tree, options: NormalizedSchema) { tags: options.parsedTags, }; - addProjectConfiguration( - host, - options.projectName, - projectConfiguration, - options.standaloneConfig - ); + 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, + projectConfiguration, + options.standaloneConfig + ); + } } function getTargets(options: NormalizedSchema) { diff --git a/packages/expo/src/generators/application/schema.d.ts b/packages/expo/src/generators/application/schema.d.ts index d62942760e071..692602357bf49 100644 --- a/packages/expo/src/generators/application/schema.d.ts +++ b/packages/expo/src/generators/application/schema.d.ts @@ -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'; } diff --git a/packages/expo/src/generators/application/schema.json b/packages/expo/src/generators/application/schema.json index 197f2a7840c05..1cea5ca793433 100644 --- a/packages/expo/src/generators/application/schema.json +++ b/packages/expo/src/generators/application/schema.json @@ -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 `/project.json` rather than including it inside `workspace.json`.", diff --git a/packages/expo/src/generators/init/init.ts b/packages/expo/src/generators/init/init.ts index 1050bff1a59a9..5e35458920d11 100644 --- a/packages/expo/src/generators/init/init.ts +++ b/packages/expo/src/generators/init/init.ts @@ -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' && diff --git a/packages/expo/src/generators/library/lib/normalize-options.ts b/packages/expo/src/generators/library/lib/normalize-options.ts index 3bd1c67964889..e0c37f2e84515 100644 --- a/packages/expo/src/generators/library/lib/normalize-options.ts +++ b/packages/expo/src/generators/library/lib/normalize-options.ts @@ -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; diff --git a/packages/expo/src/generators/library/library.spec.ts b/packages/expo/src/generators/library/library.spec.ts index 887140b01d750..674abc5d2e322 100644 --- a/packages/expo/src/generators/library/library.spec.ts +++ b/packages/expo/src/generators/library/library.spec.ts @@ -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"], diff --git a/packages/expo/src/generators/library/library.ts b/packages/expo/src/generators/library/library.ts index 2c94266ed804a..83743bf5a6e44 100644 --- a/packages/expo/src/generators/library/library.ts +++ b/packages/expo/src/generators/library/library.ts @@ -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 { - 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: {}, }; - addProjectConfiguration(host, options.name, project); + + 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 () => {}; diff --git a/packages/expo/src/generators/library/schema.json b/packages/expo/src/generators/library/schema.json index 11c7c50439b39..ea0d5c2f03838 100644 --- a/packages/expo/src/generators/library/schema.json +++ b/packages/expo/src/generators/library/schema.json @@ -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", diff --git a/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ b/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ index 0e758a58c5bc0..366365d19e53e 100644 --- a/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ +++ b/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ @@ -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": [ diff --git a/packages/jest/src/generators/configuration/lib/create-files.ts b/packages/jest/src/generators/configuration/lib/create-files.ts index 847501f93136a..e40d7ccab24fb 100644 --- a/packages/jest/src/generators/configuration/lib/create-files.ts +++ b/packages/jest/src/generators/configuration/lib/create-files.ts @@ -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, diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index 5622a87602615..86713cfc4c45e 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -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", + } + `); + }); + }); }); diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 022af8f92f468..7dbaa8afc0738 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -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, }; } } diff --git a/packages/js/src/plugins/typescript/plugin.spec.ts b/packages/js/src/plugins/typescript/plugin.spec.ts index 9c98679117552..a425dffa7c862 100644 --- a/packages/js/src/plugins/typescript/plugin.spec.ts +++ b/packages/js/src/plugins/typescript/plugin.spec.ts @@ -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", ], diff --git a/packages/js/src/plugins/typescript/plugin.ts b/packages/js/src/plugins/typescript/plugin.ts index 350d0f6835ec7..9c4cb22397ed7 100644 --- a/packages/js/src/plugins/typescript/plugin.ts +++ b/packages/js/src/plugins/typescript/plugin.ts @@ -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 = {} +): Record { + 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 = {} +): Record { + 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 = {} ): Record { 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; diff --git a/packages/js/src/utils/package-manager-workspaces.ts b/packages/js/src/utils/package-manager-workspaces.ts index f04337b6db761..50a5262808f24 100644 --- a/packages/js/src/utils/package-manager-workspaces.ts +++ b/packages/js/src/utils/package-manager-workspaces.ts @@ -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(tree, 'package.json'); + return !!packageJson?.workspaces; } export function getProjectPackageManagerWorkspaceStateWarningTask( diff --git a/packages/js/src/utils/typescript/ts-solution-setup.ts b/packages/js/src/utils/typescript/ts-solution-setup.ts index 8b544b9788ac0..d50d9fdb7c724 100644 --- a/packages/js/src/utils/typescript/ts-solution-setup.ts +++ b/packages/js/src/utils/typescript/ts-solution-setup.ts @@ -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, + 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/". + outDir: joinPathFragments('out-tsc', projectRoot.split('/').at(-1)), + rootDir, + ...compilerOptions, + }; + + const excludeSet: Set = 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; + }); + } +} diff --git a/packages/next/src/generators/application/application.spec.ts b/packages/next/src/generators/application/application.spec.ts index 0342656015707..e2c5173944b45 100644 --- a/packages/next/src/generators/application/application.spec.ts +++ b/packages/next/src/generators/application/application.spec.ts @@ -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() { diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index 30fca7bcdaf59..c17d968e74df5 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -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); } diff --git a/packages/next/src/generators/application/files/common/tsconfig.json__tmpl__ b/packages/next/src/generators/application/files/common/tsconfig.json__tmpl__ index 6ec03249199f3..e82f281c5a6f6 100644 --- a/packages/next/src/generators/application/files/common/tsconfig.json__tmpl__ +++ b/packages/next/src/generators/application/files/common/tsconfig.json__tmpl__ @@ -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", diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts index 0b37908cf0da0..6d3077927274f 100644 --- a/packages/next/src/generators/application/lib/add-e2e.ts +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -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) { }); } - addProjectConfiguration(host, options.e2eProjectName, { - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - tags: [], - implicitDependencies: [options.projectName], - }); + 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); - addProjectConfiguration(host, options.e2eProjectName, { - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - tags: [], - implicitDependencies: [options.projectName], - }); + 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, diff --git a/packages/next/src/generators/application/lib/add-linting.ts b/packages/next/src/generators/application/lib/add-linting.ts index bc131ccb51484..18ce42a6ef64a 100644 --- a/packages/next/src/generators/application/lib/add-linting.ts +++ b/packages/next/src/generators/application/lib/add-linting.ts @@ -22,6 +22,8 @@ export async function addLinting( host: Tree, options: NormalizedSchema ): Promise { + if (options.linter !== 'eslint') return () => {}; + const tasks: GeneratorCallback[] = []; tasks.push( diff --git a/packages/next/src/generators/application/lib/add-project.ts b/packages/next/src/generators/application/lib/add-project.ts index 378a9ac676cf4..49d837a9653cb 100644 --- a/packages/next/src/generators/application/lib/add-project.ts +++ b/packages/next/src/generators/application/lib/add-project.ts @@ -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 = {}; @@ -66,7 +72,26 @@ export function addProject(host: Tree, options: NormalizedSchema) { tags: options.parsedTags, }; - addProjectConfiguration(host, options.projectName, { - ...project, - }); + 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, + }); + } } diff --git a/packages/next/src/generators/application/lib/create-application-files.ts b/packages/next/src/generators/application/lib/create-application-files.ts index 143fd1840abdc..faf8f7868b234 100644 --- a/packages/next/src/generators/application/lib/create-application-files.ts +++ b/packages/next/src/generators/application/lib/create-application-files.ts @@ -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 - ? options.src - ? 'src/' - : options.appDir - ? 'app/' - : 'pages/' - : ''; + 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 diff --git a/packages/next/src/generators/application/schema.d.ts b/packages/next/src/generators/application/schema.d.ts index 205f3ed4d1467..eea51daa2b2a1 100644 --- a/packages/next/src/generators/application/schema.d.ts +++ b/packages/next/src/generators/application/schema.d.ts @@ -17,6 +17,9 @@ export interface Schema { skipPackageJson?: boolean; appDir?: boolean; src?: boolean; + // Internal options rootProject?: boolean; addPlugin?: boolean; + useTsSolution?: boolean; + formatter?: 'prettier' | 'none'; } diff --git a/packages/next/src/generators/application/schema.json b/packages/next/src/generators/application/schema.json index b9a0a81af2a33..b28be3e05a215 100644 --- a/packages/next/src/generators/application/schema.json +++ b/packages/next/src/generators/application/schema.json @@ -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", diff --git a/packages/next/src/generators/custom-server/custom-server.ts b/packages/next/src/generators/custom-server/custom-server.ts index 48b34f8e4623e..4c2e6af45d42c 100644 --- a/packages/next/src/generators/custom-server/custom-server.ts +++ b/packages/next/src/generators/custom-server/custom-server.ts @@ -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: '', }); diff --git a/packages/next/src/generators/custom-server/files/tsconfig.server.json__tmpl__ b/packages/next/src/generators/custom-server/files/tsconfig.server.json__tmpl__ index add47ecc0077a..cb1f0313bf4fb 100644 --- a/packages/next/src/generators/custom-server/files/tsconfig.server.json__tmpl__ +++ b/packages/next/src/generators/custom-server/files/tsconfig.server.json__tmpl__ @@ -1,7 +1,8 @@ { - "extends": "./tsconfig.json", + "extends": "<%= baseTsConfigPath %>", "compilerOptions": { - "module": "commonjs", + "module": "nodenext", + "moduleResolution": "nodenext", "noEmit": false, "incremental": true, <% if(hasPlugin && compiler === 'tsc') { %> diff --git a/packages/next/src/generators/init/init.ts b/packages/next/src/generators/init/init.ts index b4796f64b0655..5d0c5e1faa865 100644 --- a/packages/next/src/generators/init/init.ts +++ b/packages/next/src/generators/init/init.ts @@ -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' && diff --git a/packages/next/src/generators/library/library.ts b/packages/next/src/generators/library/library.ts index f785ab1132e3e..0269a3cf36a2c 100644 --- a/packages/next/src/generators/library/library.ts +++ b/packages/next/src/generators/library/library.ts @@ -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); } diff --git a/packages/next/src/generators/library/schema.d.ts b/packages/next/src/generators/library/schema.d.ts index 72c7a282a4504..8418bdd0ede74 100644 --- a/packages/next/src/generators/library/schema.d.ts +++ b/packages/next/src/generators/library/schema.d.ts @@ -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; diff --git a/packages/next/src/generators/library/schema.json b/packages/next/src/generators/library/schema.json index 76fbfe2338c19..4ac42721138fa 100644 --- a/packages/next/src/generators/library/schema.json +++ b/packages/next/src/generators/library/schema.json @@ -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", diff --git a/packages/playwright/src/generators/configuration/configuration.ts b/packages/playwright/src/generators/configuration/configuration.ts index dab57e23d9e98..e245e8c4d2296 100644 --- a/packages/playwright/src/generators/configuration/configuration.ts +++ b/packages/playwright/src/generators/configuration/configuration.ts @@ -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); } diff --git a/packages/react-native/src/generators/application/application.spec.ts b/packages/react-native/src/generators/application/application.spec.ts index 6dc0ea743aa17..5d2381ef171d1 100644 --- a/packages/react-native/src/generators/application/application.spec.ts +++ b/packages/react-native/src/generators/application/application.spec.ts @@ -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", + }, + ], + } + `); + }); + }); }); diff --git a/packages/react-native/src/generators/application/application.ts b/packages/react-native/src/generators/application/application.ts index 69a39350d0ecc..5518a43a0347a 100644 --- a/packages/react-native/src/generators/application/application.ts +++ b/packages/react-native/src/generators/application/application.ts @@ -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 { - 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); } diff --git a/packages/react-native/src/generators/application/lib/add-project.ts b/packages/react-native/src/generators/application/lib/add-project.ts index 6d9563c450ba2..b61a98b34c980 100644 --- a/packages/react-native/src/generators/application/lib/add-project.ts +++ b/packages/react-native/src/generators/application/lib/add-project.ts @@ -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,9 +27,24 @@ export function addProject(host: Tree, options: NormalizedSchema) { tags: options.parsedTags, }; - addProjectConfiguration(host, options.projectName, { - ...project, - }); + 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) { diff --git a/packages/react-native/src/generators/application/schema.d.ts b/packages/react-native/src/generators/application/schema.d.ts index b0066e4bd8a99..c7fba6dec1bf9 100644 --- a/packages/react-native/src/generators/application/schema.d.ts +++ b/packages/react-native/src/generators/application/schema.d.ts @@ -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'; } diff --git a/packages/react-native/src/generators/application/schema.json b/packages/react-native/src/generators/application/schema.json index a788cc695dd87..8bbc3a51c112e 100644 --- a/packages/react-native/src/generators/application/schema.json +++ b/packages/react-native/src/generators/application/schema.json @@ -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", diff --git a/packages/react-native/src/generators/init/init.ts b/packages/react-native/src/generators/init/init.ts index aba59a20254d6..c4719d5bf6e95 100644 --- a/packages/react-native/src/generators/init/init.ts +++ b/packages/react-native/src/generators/init/init.ts @@ -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); diff --git a/packages/react-native/src/generators/library/lib/normalize-options.ts b/packages/react-native/src/generators/library/lib/normalize-options.ts index ec916e3aa2c70..b98a0c2a932ac 100644 --- a/packages/react-native/src/generators/library/lib/normalize-options.ts +++ b/packages/react-native/src/generators/library/lib/normalize-options.ts @@ -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; diff --git a/packages/react-native/src/generators/library/library.spec.ts b/packages/react-native/src/generators/library/library.spec.ts index d9c0d403381e0..31d182ce6ca6c 100644 --- a/packages/react-native/src/generators/library/library.spec.ts +++ b/packages/react-native/src/generators/library/library.spec.ts @@ -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"], diff --git a/packages/react-native/src/generators/library/library.ts b/packages/react-native/src/generators/library/library.ts index 7e5a8b3d4f3d7..227cd2d302431 100644 --- a/packages/react-native/src/generators/library/library.ts +++ b/packages/react-native/src/generators/library/library.ts @@ -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 { - 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: {}, }; - addProjectConfiguration(host, options.name, project); + 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 () => {}; diff --git a/packages/react-native/src/generators/library/schema.json b/packages/react-native/src/generators/library/schema.json index 5ba6c57225040..41760ef45c23f 100644 --- a/packages/react-native/src/generators/library/schema.json +++ b/packages/react-native/src/generators/library/schema.json @@ -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", diff --git a/packages/react-native/src/generators/web-configuration/web-configuration.ts b/packages/react-native/src/generators/web-configuration/web-configuration.ts index 85204f46cf030..4903940df5758 100644 --- a/packages/react-native/src/generators/web-configuration/web-configuration.ts +++ b/packages/react-native/src/generators/web-configuration/web-configuration.ts @@ -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, }); diff --git a/packages/react-native/src/utils/versions.ts b/packages/react-native/src/utils/versions.ts index c209303b42865..aefe1624744cc 100644 --- a/packages/react-native/src/utils/versions.ts +++ b/packages/react-native/src/utils/versions.ts @@ -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'; diff --git a/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap index f971b87d9db33..0a05b6eb0842d 100644 --- a/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/react/src/generators/application/__snapshots__/application.spec.ts.snap @@ -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() { diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index d94a8268388e7..874abcde48376 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -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", + }, + ], + } + `); + }); + }); }); diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index 283e72c7a19a3..c9a421896e46a 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -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 { - 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); } diff --git a/packages/react/src/generators/application/files/base-rspack/tsconfig.app.json__tmpl__ b/packages/react/src/generators/application/files/base-rspack/tsconfig.app.json__tmpl__ index 8bdce3f07d6ac..8b1bc8bb50853 100644 --- a/packages/react/src/generators/application/files/base-rspack/tsconfig.app.json__tmpl__ +++ b/packages/react/src/generators/application/files/base-rspack/tsconfig.app.json__tmpl__ @@ -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"] } +<% } %> diff --git a/packages/react/src/generators/application/files/base-vite/tsconfig.app.json__tmpl__ b/packages/react/src/generators/application/files/base-vite/tsconfig.app.json__tmpl__ index 80a1b8ebf4c4a..8b1bc8bb50853 100644 --- a/packages/react/src/generators/application/files/base-vite/tsconfig.app.json__tmpl__ +++ b/packages/react/src/generators/application/files/base-vite/tsconfig.app.json__tmpl__ @@ -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", @@ -7,8 +23,9 @@ <%_ 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"] } +<% } %> diff --git a/packages/react/src/generators/application/files/base-webpack/tsconfig.app.json__tmpl__ b/packages/react/src/generators/application/files/base-webpack/tsconfig.app.json__tmpl__ index 03309c7d0f926..8b1bc8bb50853 100644 --- a/packages/react/src/generators/application/files/base-webpack/tsconfig.app.json__tmpl__ +++ b/packages/react/src/generators/application/files/base-webpack/tsconfig.app.json__tmpl__ @@ -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", @@ -7,8 +23,9 @@ <%_ 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"] } +<% } %> diff --git a/packages/react/src/generators/application/files/style-css-module/src/app/__fileName__.tsx__tmpl__ b/packages/react/src/generators/application/files/style-css-module/src/app/__fileName__.tsx__tmpl__ index 4f04c1a17dbca..59f0f480112ee 100644 --- a/packages/react/src/generators/application/files/style-css-module/src/app/__fileName__.tsx__tmpl__ +++ b/packages/react/src/generators/application/files/style-css-module/src/app/__fileName__.tsx__tmpl__ @@ -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"; <%_ } _%> diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts index e84ecea5f57a5..2f43036fb6953 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -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,14 +83,31 @@ export async function addE2e( typeof import('@nx/cypress') >('@nx/cypress', nxVersion); - addProjectConfiguration(tree, options.e2eProjectName, { - projectType: 'application', - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - implicitDependencies: [options.projectName], - tags: [], - }); + 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, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + implicitDependencies: [options.projectName], + tags: [], + }); + } const e2eTask = await configurationGenerator(tree, { ...options, @@ -157,13 +176,31 @@ export async function addE2e( const { configurationGenerator } = ensurePackage< typeof import('@nx/playwright') >('@nx/playwright', nxVersion); - addProjectConfiguration(tree, options.e2eProjectName, { - projectType: 'application', - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - implicitDependencies: [options.projectName], - }); + 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, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + implicitDependencies: [options.projectName], + }); + } + const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, skipFormat: true, diff --git a/packages/react/src/generators/application/lib/add-jest.ts b/packages/react/src/generators/application/lib/add-jest.ts index aceef21632f08..23f3af529f6ef 100644 --- a/packages/react/src/generators/application/lib/add-jest.ts +++ b/packages/react/src/generators/application/lib/add-jest.ts @@ -23,5 +23,6 @@ export async function addJest( setupFile: 'none', compiler: options.compiler, skipFormat: true, + runtimeTsconfigFileName: 'tsconfig.app.json', }); } diff --git a/packages/react/src/generators/application/lib/add-project.ts b/packages/react/src/generators/application/lib/add-project.ts index fd494e64a1de0..a4cee10a39ac8 100644 --- a/packages/react/src/generators/application/lib/add-project.ts +++ b/packages/react/src/generators/application/lib/add-project.ts @@ -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,9 +38,25 @@ export function addProject(host: Tree, options: NormalizedSchema) { }; } - addProjectConfiguration(host, options.projectName, { - ...project, - }); + 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( diff --git a/packages/react/src/generators/application/lib/create-application-files.ts b/packages/react/src/generators/application/lib/create-application-files.ts index dbc6071933703..d84b28be9aead 100644 --- a/packages/react/src/generators/application/lib/create-application-files.ts +++ b/packages/react/src/generators/application/lib/create-application-files.ts @@ -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') { diff --git a/packages/react/src/generators/application/lib/normalize-options.ts b/packages/react/src/generators/application/lib/normalize-options.ts index 6cb6a6080ef67..37768a6a0d36e 100644 --- a/packages/react/src/generators/application/lib/normalize-options.ts +++ b/packages/react/src/generators/application/lib/normalize-options.ts @@ -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( fileName, styledModule, hasStyles: options.style !== 'none', + isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), } as NormalizedSchema; normalized.routing = normalized.routing ?? false; diff --git a/packages/react/src/generators/application/lib/update-jest-config.ts b/packages/react/src/generators/application/lib/update-jest-config.ts index 62e279dc87a4a..84eb70612a07b 100644 --- a/packages/react/src/generators/application/lib/update-jest-config.ts +++ b/packages/react/src/generators/application/lib/update-jest-config.ts @@ -20,6 +20,11 @@ export function updateSpecConfig(host: Tree, options: NormalizedSchema) { ); compilerOptions.types = types; json.compilerOptions = compilerOptions; + if (options.isUsingTsSolutionConfig) { + // add project reference to the runtime tsconfig.app.json file + json.references ??= []; + json.references.push({ path: './tsconfig.app.json' }); + } return json; }); diff --git a/packages/react/src/generators/application/schema.d.ts b/packages/react/src/generators/application/schema.d.ts index c0acac95870e0..0dc79aaf1dfbb 100644 --- a/packages/react/src/generators/application/schema.d.ts +++ b/packages/react/src/generators/application/schema.d.ts @@ -25,8 +25,12 @@ export interface Schema { rootProject?: boolean; bundler?: 'webpack' | 'vite' | 'rspack'; minimal?: boolean; + // Internal options addPlugin?: boolean; nxCloudToken?: string; + useTsSolution?: boolean; + formatter?: 'prettier' | 'none'; + alwaysGenerateProjectJson?: boolean; // this is needed for MF currently } export interface NormalizedSchema extends T { @@ -40,4 +44,5 @@ export interface NormalizedSchema extends T { hasStyles: boolean; unitTestRunner: 'jest' | 'vitest' | 'none'; addPlugin?: boolean; + isUsingTsSolutionConfig?: boolean; } diff --git a/packages/react/src/generators/application/schema.json b/packages/react/src/generators/application/schema.json index 0e331b3bccb9d..48c0dffa5d9da 100644 --- a/packages/react/src/generators/application/schema.json +++ b/packages/react/src/generators/application/schema.json @@ -80,12 +80,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.", @@ -104,11 +98,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", @@ -171,14 +183,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", diff --git a/packages/react/src/generators/component/component.spec.ts b/packages/react/src/generators/component/component.spec.ts index 534648b3b3e40..76555e45f042d 100644 --- a/packages/react/src/generators/component/component.spec.ts +++ b/packages/react/src/generators/component/component.spec.ts @@ -162,7 +162,7 @@ describe('component', () => { const indexContent = appTree.read('my-lib/src/index.ts', 'utf-8'); - expect(indexContent).not.toMatch(/lib\/hello/); + expect(indexContent).toMatch(/lib\/hello/); }); }); diff --git a/packages/react/src/generators/component/component.ts b/packages/react/src/generators/component/component.ts index 5c5473d8f301f..d532e074062fb 100644 --- a/packages/react/src/generators/component/component.ts +++ b/packages/react/src/generators/component/component.ts @@ -106,10 +106,17 @@ function addExportsToBarrel(host: Tree, options: NormalizedSchema) { workspace.get(options.projectName).projectType === 'application'; if (options.export && !isApp) { - const indexFilePath = joinPathFragments( - options.projectSourceRoot, - options.js ? 'index.js' : 'index.ts' - ); + const indexFilePath = options.projectSourceRoot + ? joinPathFragments( + options.projectSourceRoot, + options.js ? 'index.js' : 'index.ts' + ) + : joinPathFragments( + options.projectRoot, + 'src', + options.js ? 'index.js' : 'index.ts' + ); + const indexSource = host.read(indexFilePath, 'utf-8'); if (indexSource !== null) { const indexSourceFile = tsModule.createSourceFile( diff --git a/packages/react/src/generators/component/lib/normalize-options.ts b/packages/react/src/generators/component/lib/normalize-options.ts index c61b1409525c6..3ea86c7d1c493 100644 --- a/packages/react/src/generators/component/lib/normalize-options.ts +++ b/packages/react/src/generators/component/lib/normalize-options.ts @@ -61,6 +61,7 @@ export async function normalizeOptions( className, fileName, filePath, - projectSourceRoot: projectSourceRoot ?? projectRoot, + projectRoot, + projectSourceRoot: projectSourceRoot, }; } diff --git a/packages/react/src/generators/component/schema.d.ts b/packages/react/src/generators/component/schema.d.ts index f3860e29acadf..f6558748bcd16 100644 --- a/packages/react/src/generators/component/schema.d.ts +++ b/packages/react/src/generators/component/schema.d.ts @@ -19,6 +19,7 @@ export interface Schema { export interface NormalizedSchema extends Schema { directory: string; + projectRoot: string; projectSourceRoot: string; projectName: string; fileName: string; diff --git a/packages/react/src/generators/host/host.rspack.spec.ts b/packages/react/src/generators/host/host.rspack.spec.ts index 7aaec551dbf78..032ab68ce10d6 100644 --- a/packages/react/src/generators/host/host.rspack.spec.ts +++ b/packages/react/src/generators/host/host.rspack.spec.ts @@ -1,5 +1,5 @@ import * as devkit from '@nx/devkit'; -import type { Tree } from '@nx/devkit'; +import { Tree, updateJson, writeJson } from '@nx/devkit'; import { ProjectGraph, readJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import hostGenerator from './host'; @@ -426,4 +426,126 @@ describe('hostGenerator', () => { `); }); }); + + 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 hostGenerator(tree, { + directory: 'myapp', + addPlugin: true, + remotes: ['remote1', 'remote2', 'remote3'], + e2eTestRunner: 'none', + linter: Linter.None, + style: 'css', + unitTestRunner: 'none', + typescriptConfiguration: false, + bundler: 'rspack', + }); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./myapp", + }, + { + "path": "./remote1", + }, + { + "path": "./remote2", + }, + { + "path": "./remote3", + }, + ] + `); + expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "../remote1", + }, + { + "path": "../remote2", + }, + { + "path": "../remote3", + }, + ], + } + `); + expect(readJson(tree, '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", + ], + }, + "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", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx", + ], + "references": [ + { + "path": "../remote1/tsconfig.app.json", + }, + { + "path": "../remote2/tsconfig.app.json", + }, + { + "path": "../remote3/tsconfig.app.json", + }, + ], + } + `); + }); + }); }); diff --git a/packages/react/src/generators/host/host.ts b/packages/react/src/generators/host/host.ts index 84ae83e851e3b..fcd302431dc26 100644 --- a/packages/react/src/generators/host/host.ts +++ b/packages/react/src/generators/host/host.ts @@ -28,14 +28,12 @@ import { nxVersion, } from '../../utils/versions'; import { ensureProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { updateModuleFederationTsconfig } from './lib/update-module-federation-tsconfig'; export async function hostGenerator( host: Tree, schema: Schema ): Promise { - assertNotUsingTsSolutionSetup(host, 'react', 'host'); - const tasks: GeneratorCallback[] = []; const options: NormalizedSchema = { ...(await normalizeOptions(host, schema)), @@ -70,6 +68,7 @@ export async function hostGenerator( // The target use-case is loading remotes as child routes, thus always enable routing. routing: true, skipFormat: true, + alwaysGenerateProjectJson: true, }); tasks.push(initTask); @@ -106,6 +105,7 @@ export async function hostGenerator( addModuleFederationFiles(host, options, remotesWithPorts); updateModuleFederationProject(host, options); updateModuleFederationE2eProject(host, options); + updateModuleFederationTsconfig(host, options); if (options.ssr) { const setupSsrTask = await setupSsrGenerator(host, { diff --git a/packages/react/src/generators/host/lib/update-module-federation-tsconfig.ts b/packages/react/src/generators/host/lib/update-module-federation-tsconfig.ts new file mode 100644 index 0000000000000..1cd0b4cf6168d --- /dev/null +++ b/packages/react/src/generators/host/lib/update-module-federation-tsconfig.ts @@ -0,0 +1,48 @@ +import { joinPathFragments, type Tree, updateJson } from '@nx/devkit'; +import { NormalizedSchema } from '../schema'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; + +export function updateModuleFederationTsconfig( + host: Tree, + options: NormalizedSchema +) { + const tsconfigPath = joinPathFragments( + options.appProjectRoot, + 'tsconfig.json' + ); + const tsconfigRuntimePath = joinPathFragments( + options.appProjectRoot, + 'tsconfig.app.json' + ); + if (!host.exists(tsconfigPath) || !host.exists(tsconfigRuntimePath)) return; + + // Not setting `baseUrl` does not work with MF. + if (isUsingTsSolutionSetup(host)) { + updateJson(host, 'tsconfig.base.json', (json) => { + json.compilerOptions.baseUrl = '.'; + return json; + }); + + // Update references to match what `nx sync` does. + if (options.remotes?.length) { + updateJson(host, tsconfigPath, (json) => { + json.references ??= []; + for (const remote of options.remotes) { + const remotePath = `../${remote}`; + if (!json.references.some((ref) => ref.path === remotePath)) + json.references.push({ path: remotePath }); + } + return json; + }); + updateJson(host, tsconfigRuntimePath, (json) => { + json.references ??= []; + for (const remote of options.remotes) { + const remotePath = `../${remote}/tsconfig.app.json`; + if (!json.references.some((ref) => ref.path === remotePath)) + json.references.push({ path: remotePath }); + } + return json; + }); + } + } +} diff --git a/packages/react/src/generators/init/init.ts b/packages/react/src/generators/init/init.ts index 2f63ee51df959..105e863d62793 100755 --- a/packages/react/src/generators/init/init.ts +++ b/packages/react/src/generators/init/init.ts @@ -6,13 +6,10 @@ import { type GeneratorCallback, type Tree, } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { nxVersion, reactDomVersion, reactVersion } from '../../utils/versions'; import { InitSchema } from './schema'; export async function reactInitGenerator(host: Tree, schema: InitSchema) { - assertNotUsingTsSolutionSetup(host, 'react', 'init'); - const tasks: GeneratorCallback[] = []; if (!schema.skipPackageJson) { diff --git a/packages/react/src/generators/library/files/common/package.json__tmpl__ b/packages/react/src/generators/library/files/common/package.json__tmpl__ deleted file mode 100644 index fa518765a372f..0000000000000 --- a/packages/react/src/generators/library/files/common/package.json__tmpl__ +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "<%= name %>", - "version": "0.0.1" -} diff --git a/packages/react/src/generators/library/files/vite/package.json__tmpl__ b/packages/react/src/generators/library/files/vite/package.json__tmpl__ deleted file mode 100644 index 507420ee30834..0000000000000 --- a/packages/react/src/generators/library/files/vite/package.json__tmpl__ +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "<%= name %>", - "version": "0.0.1", - "main": "./index.js", - "types": "./index.d.ts", - "exports": { - ".": { - "import": "./index.mjs", - "require": "./index.js" - } - } -} diff --git a/packages/react/src/generators/library/lib/create-files.ts b/packages/react/src/generators/library/lib/create-files.ts index e5163ff7c6c2b..e873b26d7ecc1 100644 --- a/packages/react/src/generators/library/lib/create-files.ts +++ b/packages/react/src/generators/library/lib/create-files.ts @@ -1,10 +1,10 @@ -import type { Tree } from '@nx/devkit'; import { generateFiles, joinPathFragments, names, offsetFromRoot, toJS, + Tree, writeJson, } from '@nx/devkit'; import { getRelativePathToRootTsConfig } from '@nx/js'; @@ -68,8 +68,29 @@ export function createFiles(host: Tree, options: NormalizedSchema) { }); } - if (!options.publishable && !options.buildable) { - host.delete(`${options.projectRoot}/package.json`); + if ( + (options.publishable || options.buildable) && + !options.isUsingTsSolutionConfig + ) { + if (options.bundler === 'vite') { + writeJson(host, `${options.projectRoot}/package.json`, { + name: options.importPath, + version: '0.0.1', + main: './index.js', + types: './index.d.ts', + exports: { + '.': { + import: './index.mjs', + require: './index.js', + }, + }, + }); + } else { + writeJson(host, `${options.projectRoot}/package.json`, { + name: options.importPath, + version: '0.0.1', + }); + } } if (options.js) { diff --git a/packages/react/src/generators/library/lib/normalize-options.ts b/packages/react/src/generators/library/lib/normalize-options.ts index 389c2b74cd93b..76233f3dd833b 100644 --- a/packages/react/src/generators/library/lib/normalize-options.ts +++ b/packages/react/src/generators/library/lib/normalize-options.ts @@ -12,11 +12,15 @@ import { } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { assertValidStyle } from '../../../utils/assertion'; import { NormalizedSchema, Schema } from '../schema'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; export async function normalizeOptions( host: Tree, options: Schema ): Promise { + const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); + await ensureProjectName(host, options, 'library'); const { projectName, @@ -104,6 +108,8 @@ export async function normalizeOptions( assertValidStyle(normalized.style); + normalized.isUsingTsSolutionConfig = isUsingTsSolutionConfig; + return normalized; } diff --git a/packages/react/src/generators/library/library.spec.ts b/packages/react/src/generators/library/library.spec.ts index ae4617f35c4f5..60ad9f8216236 100644 --- a/packages/react/src/generators/library/library.spec.ts +++ b/packages/react/src/generators/library/library.spec.ts @@ -7,6 +7,7 @@ import { readProjectConfiguration, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '@nx/eslint'; @@ -875,4 +876,190 @@ module.exports = withNx( }); } ); + + 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 libraryGenerator(tree, { + ...defaultSchema, + bundler: 'vite', + unitTestRunner: 'vitest', + directory: 'mylib', + name: 'mylib', + }); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./mylib", + }, + ] + `); + expect(readJson(tree, 'mylib/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'mylib/tsconfig.lib.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "out-tsc/mylib", + "rootDir": "src", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "vite/client", + ], + }, + "exclude": [ + "dist", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "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", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx", + ], + } + `); + expect(readJson(tree, 'mylib/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", + ], + }, + "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.lib.json", + }, + ], + } + `); + }); + + it('should map non-buildable libraries to source', async () => { + await libraryGenerator(tree, { + ...defaultSchema, + bundler: 'none', + unitTestRunner: 'none', + directory: 'mylib', + name: 'mylib', + }); + + await libraryGenerator(tree, { + ...defaultSchema, + bundler: 'none', + unitTestRunner: 'none', + directory: 'myjslib', + name: 'myjslib', + js: true, + }); + + expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(` + { + "main": "./src/index.ts", + "name": "@proj/mylib", + "nx": { + "name": "mylib", + "projectType": "library", + "sourceRoot": "mylib/src", + }, + "types": "./src/index.ts", + } + `); + expect(readJson(tree, 'myjslib/package.json')).toMatchInlineSnapshot(` + { + "main": "./src/index.js", + "name": "@proj/myjslib", + "nx": { + "name": "myjslib", + "projectType": "library", + "sourceRoot": "myjslib/src", + }, + "types": "./src/index.js", + } + `); + }); + }); }); diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index 337d90cb451b4..7a33143c2cf7c 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -4,15 +4,16 @@ import { ensurePackage, formatFiles, GeneratorCallback, + installPackagesTask, joinPathFragments, runTasksInSerial, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { getRelativeCwd } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { addTsConfigPath, initGenerator as jsInitGenerator } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { nxVersion } from '../../utils/versions'; import { maybeJs } from '../../utils/maybe-js'; @@ -28,6 +29,7 @@ import { createFiles } from './lib/create-files'; import { extractTsConfigBase } from '../../utils/create-ts-config'; import { installCommonDependencies } from './lib/install-common-dependencies'; import { setDefaults } from './lib/set-defaults'; +import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; export async function libraryGenerator(host: Tree, schema: Schema) { return await libraryGeneratorInternal(host, { @@ -37,10 +39,14 @@ export async function libraryGenerator(host: Tree, schema: Schema) { } export async function libraryGeneratorInternal(host: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(host, 'react', '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) { throw new Error( @@ -51,31 +57,45 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) { options.style = 'none'; } - const jsInitTask = await jsInitGenerator(host, { - ...schema, - skipFormat: true, - }); - tasks.push(jsInitTask); - const initTask = await initGenerator(host, { ...options, skipFormat: true, }); tasks.push(initTask); - addProjectConfiguration(host, options.name, { - root: options.projectRoot, - sourceRoot: joinPathFragments(options.projectRoot, 'src'), - projectType: 'library', - tags: options.parsedTags, - targets: {}, - }); + if (options.isUsingTsSolutionConfig) { + const sourceEntry = + options.bundler === 'none' + ? options.js + ? './src/index.js' + : './src/index.ts' + : undefined; + writeJson(host, `${options.projectRoot}/package.json`, { + name: options.importPath, + main: sourceEntry, + types: sourceEntry, + nx: { + name: options.importPath === options.name ? undefined : options.name, + projectType: 'library', + sourceRoot: `${options.projectRoot}/src`, + tags: options.parsedTags?.length ? options.parsedTags : undefined, + }, + }); + } else { + addProjectConfiguration(host, options.name, { + root: options.projectRoot, + sourceRoot: joinPathFragments(options.projectRoot, 'src'), + projectType: 'library', + tags: options.parsedTags, + targets: {}, + }); + } + + createFiles(host, options); const lintTask = await addLinting(host, options); tasks.push(lintTask); - createFiles(host, options); - // Set up build target if (options.buildable && options.bundler === 'vite') { const { viteConfigurationGenerator, createOrEditViteConfig } = @@ -228,7 +248,7 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) { extractTsConfigBase(host); - if (!options.skipTsConfig) { + if (!options.skipTsConfig && !options.isUsingTsSolutionConfig) { addTsConfigPath(host, options.importPath, [ maybeJs( options, @@ -237,10 +257,29 @@ export async function libraryGeneratorInternal(host: Tree, schema: 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); } + // Always run install to link packages. + if (options.isUsingTsSolutionConfig) { + tasks.push(() => installPackagesTask(host)); + } + tasks.push(() => { logShowProjectCommand(options.name); }); diff --git a/packages/react/src/generators/library/schema.d.ts b/packages/react/src/generators/library/schema.d.ts index 8bcd42b369ea0..0b23eb2f168be 100644 --- a/packages/react/src/generators/library/schema.d.ts +++ b/packages/react/src/generators/library/schema.d.ts @@ -39,4 +39,5 @@ export interface NormalizedSchema extends Schema { appMain?: string; appSourceRoot?: string; unitTestRunner: 'jest' | 'vitest' | 'none'; + isUsingTsSolutionConfig?: boolean; } diff --git a/packages/react/src/generators/library/schema.json b/packages/react/src/generators/library/schema.json index cdac0a30d5271..dc4b31374b528 100644 --- a/packages/react/src/generators/library/schema.json +++ b/packages/react/src/generators/library/schema.json @@ -71,18 +71,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", @@ -154,14 +165,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"], diff --git a/packages/react/src/generators/remote/remote.ts b/packages/react/src/generators/remote/remote.ts index 4d6549715c207..88cde9458e483 100644 --- a/packages/react/src/generators/remote/remote.ts +++ b/packages/react/src/generators/remote/remote.ts @@ -31,7 +31,6 @@ import { nxVersion, } from '../../utils/versions'; import { ensureProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function addModuleFederationFiles( host: Tree, @@ -95,8 +94,6 @@ export function addModuleFederationFiles( } export async function remoteGenerator(host: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(host, 'react', 'remote'); - const tasks: GeneratorCallback[] = []; const options: NormalizedSchema = { ...(await normalizeOptions(host, schema)), @@ -137,6 +134,7 @@ export async function remoteGenerator(host: Tree, schema: Schema) { ...options, name: options.projectName, skipFormat: true, + alwaysGenerateProjectJson: true, }); tasks.push(initAppTask); diff --git a/packages/react/src/rules/update-module-federation-project.ts b/packages/react/src/rules/update-module-federation-project.ts index 350d047506d03..3a8455d16aade 100644 --- a/packages/react/src/rules/update-module-federation-project.ts +++ b/packages/react/src/rules/update-module-federation-project.ts @@ -8,6 +8,7 @@ import { } from '@nx/devkit'; import { nxVersion } from '../utils/versions'; import { maybeJs } from '../utils/maybe-js'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function updateModuleFederationProject( host: Tree, @@ -125,5 +126,11 @@ export function updateModuleFederationProject( }, }; + // Typechecks must be performed first before build and serve to generate remote d.ts files. + if (isUsingTsSolutionSetup(host)) { + projectConfig.targets.build.dependsOn = ['^build', 'typecheck']; + projectConfig.targets.serve.dependsOn = ['typecheck']; + } + updateProjectConfiguration(host, options.projectName, projectConfig); } diff --git a/packages/react/src/utils/create-ts-config.ts b/packages/react/src/utils/create-ts-config.ts index a86d0ca52b53c..921dccfbbbb01 100644 --- a/packages/react/src/utils/create-ts-config.ts +++ b/packages/react/src/utils/create-ts-config.ts @@ -1,6 +1,7 @@ import { Tree } from 'nx/src/generators/tree'; import * as shared from '@nx/js/src/utils/typescript/create-ts-config'; import { updateJson, writeJson } from 'nx/src/generators/utils/json'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function createTsConfig( host: Tree, @@ -14,6 +15,110 @@ export function createTsConfig( unitTestRunner?: string; }, relativePathToRootTsConfig: string +) { + if (isUsingTsSolutionSetup(host)) { + createTsConfigForTsSolution( + host, + projectRoot, + type, + options, + relativePathToRootTsConfig + ); + } else { + createTsConfigForNonTsSolution( + host, + projectRoot, + type, + options, + relativePathToRootTsConfig + ); + } +} + +export function extractTsConfigBase(host: Tree) { + shared.extractTsConfigBase(host); + + if (host.exists('vite.config.ts')) { + const vite = host.read('vite.config.ts').toString(); + host.write( + 'vite.config.ts', + vite.replace(`projects: []`, `projects: ['tsconfig.base.json']`) + ); + } +} + +function createTsConfigForTsSolution( + host: Tree, + projectRoot: string, + type: 'app' | 'lib', + options: { + strict?: boolean; + style?: string; + bundler?: string; + rootProject?: boolean; + unitTestRunner?: string; + }, + relativePathToRootTsConfig: string +) { + const json = { + files: [], + include: [], + references: [ + { + path: type === 'app' ? './tsconfig.app.json' : './tsconfig.lib.json', + }, + ], + } as any; + + // inline tsconfig.base.json into the project + if (options.rootProject) { + json.compileOnSave = false; + json.compilerOptions = { + ...shared.tsConfigBaseOptions, + ...json.compilerOptions, + }; + json.exclude = ['node_modules', 'tmp']; + } else { + json.extends = relativePathToRootTsConfig; + } + + writeJson(host, `${projectRoot}/tsconfig.json`, json); + + const tsconfigProjectPath = `${projectRoot}/tsconfig.${type}.json`; + if (host.exists(tsconfigProjectPath)) { + updateJson(host, tsconfigProjectPath, (json) => { + if (options.bundler === 'vite') { + json.compilerOptions ??= {}; + + const types = new Set(json.compilerOptions.types ?? []); + types.add('node'); + types.add('vite/client'); + + json.compilerOptions.types = Array.from(types); + } + + if (options.style === '@emotion/styled') { + json.compilerOptions ??= {}; + json.compilerOptions.jsxImportSource = '@emotion/react'; + } + + return json; + }); + } +} + +function createTsConfigForNonTsSolution( + host: Tree, + projectRoot: string, + type: 'app' | 'lib', + options: { + strict?: boolean; + style?: string; + bundler?: string; + rootProject?: boolean; + unitTestRunner?: string; + }, + relativePathToRootTsConfig: string ) { const json = { compilerOptions: { @@ -72,15 +177,3 @@ export function createTsConfig( }); } } - -export function extractTsConfigBase(host: Tree) { - shared.extractTsConfigBase(host); - - if (host.exists('vite.config.ts')) { - const vite = host.read('vite.config.ts').toString(); - host.write( - 'vite.config.ts', - vite.replace(`projects: []`, `projects: ['tsconfig.base.json']`) - ); - } -} diff --git a/packages/react/src/utils/versions.ts b/packages/react/src/utils/versions.ts index 1e387e083a569..782a8b62d69d4 100755 --- a/packages/react/src/utils/versions.ts +++ b/packages/react/src/utils/versions.ts @@ -1,8 +1,5 @@ export const nxVersion = require('../../package.json').version; -// Always pull the latest version until we merge rspack plugin into the repo. -export const nxRspackVersion = '*'; - export const reactVersion = '18.3.1'; export const reactDomVersion = '18.3.1'; export const reactIsVersion = '18.3.1'; diff --git a/packages/remix/src/generators/application/application.impl.spec.ts b/packages/remix/src/generators/application/application.impl.spec.ts index 7a41eefd1b469..47feaf0a0b48a 100644 --- a/packages/remix/src/generators/application/application.impl.spec.ts +++ b/packages/remix/src/generators/application/application.impl.spec.ts @@ -1,6 +1,13 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; -import { joinPathFragments, readJson, readNxJson, type Tree } from '@nx/devkit'; +import { + joinPathFragments, + readJson, + readNxJson, + type Tree, + updateJson, + writeJson, +} from '@nx/devkit'; import * as devkitExports from 'nx/src/devkit-exports'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -307,6 +314,216 @@ describe('Remix Application', () => { }); }); }); + + describe('TS solution setup', () => { + it('should add project references when using TS solution', async () => { + const 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: [], + }); + + await applicationGenerator(tree, { + directory: 'myapp', + e2eTestRunner: 'playwright', + unitTestRunner: 'jest', + addPlugin: true, + tags: 'foo', + }); + + expect(readJson(tree, 'myapp/package.json')).toMatchInlineSnapshot(` + { + "dependencies": { + "@remix-run/node": "^2.14.0", + "@remix-run/react": "^2.14.0", + "@remix-run/serve": "^2.14.0", + "isbot": "^4.4.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + }, + "devDependencies": { + "@remix-run/dev": "^2.14.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + }, + "engines": { + "node": ">=20", + }, + "name": "myapp", + "nx": { + "projectType": "application", + "sourceRoot": "myapp", + "tags": [ + "foo", + ], + }, + "private": true, + "scripts": {}, + "sideEffects": false, + "type": "module", + } + `); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./myapp-e2e", + }, + { + "path": "./myapp", + }, + ] + `); + expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.app.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": [ + "DOM", + "DOM.Iterable", + "ES2019", + ], + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "out-tsc/myapp", + "resolveJsonModule": true, + "rootDir": ".", + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": [ + "@remix-run/node", + "vite/client", + ], + }, + "exclude": [ + "dist", + "tests/**/*.spec.ts", + "tests/**/*.test.ts", + "tests/**/*.spec.tsx", + "tests/**/*.test.tsx", + "tests/**/*.spec.js", + "tests/**/*.test.js", + "tests/**/*.spec.jsx", + "tests/**/*.test.jsx", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "app/**/*.ts", + "app/**/*.tsx", + "app/**/*.js", + "app/**/*.jsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx", + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "./out-tsc/jest", + "types": [ + "jest", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "vite.config.ts", + "vitest.config.ts", + "app/**/*.ts", + "app/**/*.tsx", + "app/**/*.js", + "app/**/*.jsx", + "tests/**/*.spec.ts", + "tests/**/*.test.ts", + "tests/**/*.spec.tsx", + "tests/**/*.test.tsx", + "tests/**/*.spec.js", + "tests/**/*.test.js", + "tests/**/*.spec.jsx", + "tests/**/*.test.jsx", + ], + "references": [ + { + "path": "./tsconfig.app.json", + }, + ], + } + `); + expect(readJson(tree, '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", + }, + ], + } + `); + }); + }); }); function expectTargetsToBeCorrect(tree: Tree, projectRoot: string) { diff --git a/packages/remix/src/generators/application/application.impl.ts b/packages/remix/src/generators/application/application.impl.ts index c56633bf63556..e66b13fd2885d 100644 --- a/packages/remix/src/generators/application/application.impl.ts +++ b/packages/remix/src/generators/application/application.impl.ts @@ -17,7 +17,6 @@ import { import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { initGenerator as jsInitGenerator } from '@nx/js'; import { extractTsConfigBase } from '@nx/js/src/utils/typescript/create-ts-config'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { dirname } from 'node:path'; import { createNxCloudOnboardingURLForWelcomeApp, @@ -45,6 +44,10 @@ import { addViteTempFilesToGitIgnore, } from './lib'; import { NxRemixGeneratorSchema } from './schema'; +import { + isUsingTsSolutionSetup, + updateTsconfigFiles, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; export function remixApplicationGenerator( tree: Tree, @@ -60,7 +63,17 @@ export async function remixApplicationGeneratorInternal( tree: Tree, _options: NxRemixGeneratorSchema ) { - assertNotUsingTsSolutionSetup(tree, 'remix', 'application'); + const tasks: GeneratorCallback[] = [ + await initGenerator(tree, { + skipFormat: true, + addPlugin: true, + }), + await jsInitGenerator(tree, { + skipFormat: true, + addTsPlugin: _options.useTsSolution, + formatter: _options.formatter, + }), + ]; const options = await normalizeOptions(tree, _options); if (!options.addPlugin) { @@ -69,21 +82,17 @@ export async function remixApplicationGeneratorInternal( ); } - const tasks: GeneratorCallback[] = [ - await initGenerator(tree, { - skipFormat: true, - addPlugin: true, - }), - await jsInitGenerator(tree, { skipFormat: true }), - ]; + const isUsingTsSolution = isUsingTsSolutionSetup(tree); - addProjectConfiguration(tree, options.projectName, { - root: options.projectRoot, - sourceRoot: `${options.projectRoot}`, - projectType: 'application', - tags: options.parsedTags, - targets: {}, - }); + if (!isUsingTsSolution) { + addProjectConfiguration(tree, options.projectName, { + root: options.projectRoot, + sourceRoot: `${options.projectRoot}`, + projectType: 'application', + tags: options.parsedTags, + targets: {}, + }); + } const installTask = updateDependencies(tree); tasks.push(installTask); @@ -110,6 +119,7 @@ export async function remixApplicationGeneratorInternal( eslintVersion, typescriptVersion, viteVersion, + isUsingTsSolution, }; generateFiles( @@ -135,7 +145,16 @@ export async function remixApplicationGeneratorInternal( } else { generateFiles( tree, - joinPathFragments(__dirname, 'files/integrated'), + joinPathFragments(__dirname, 'files/non-root'), + options.projectRoot, + vars + ); + } + + if (isUsingTsSolution) { + generateFiles( + tree, + joinPathFragments(__dirname, 'files/ts-solution'), options.projectRoot, vars ); @@ -187,7 +206,7 @@ export async function remixApplicationGeneratorInternal( addPlugin: true, }); const projectConfig = readProjectConfiguration(tree, options.projectName); - if (projectConfig.targets['test']?.options) { + if (projectConfig.targets?.['test']?.options) { projectConfig.targets['test'].options.passWithNoTests = true; updateProjectConfiguration(tree, options.projectName, projectConfig); } @@ -340,6 +359,21 @@ export default {...nxPreset}; await formatFiles(tree); } + updateTsconfigFiles( + tree, + options.projectRoot, + 'tsconfig.app.json', + { + jsx: 'react-jsx', + module: 'esnext', + moduleResolution: 'bundler', + }, + options.linter === 'eslint' + ? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'] + : undefined, + '.' + ); + tasks.push(() => { logShowProjectCommand(options.projectName); }); diff --git a/packages/remix/src/generators/application/files/integrated/.gitignore__tmpl__ b/packages/remix/src/generators/application/files/non-root/.gitignore__tmpl__ similarity index 100% rename from packages/remix/src/generators/application/files/integrated/.gitignore__tmpl__ rename to packages/remix/src/generators/application/files/non-root/.gitignore__tmpl__ diff --git a/packages/remix/src/generators/application/files/integrated/package.json__tmpl__ b/packages/remix/src/generators/application/files/non-root/package.json__tmpl__ similarity index 100% rename from packages/remix/src/generators/application/files/integrated/package.json__tmpl__ rename to packages/remix/src/generators/application/files/non-root/package.json__tmpl__ diff --git a/packages/remix/src/generators/application/files/ts-solution/package.json__tmpl__ b/packages/remix/src/generators/application/files/ts-solution/package.json__tmpl__ new file mode 100644 index 0000000000000..06b3362f5c0fb --- /dev/null +++ b/packages/remix/src/generators/application/files/ts-solution/package.json__tmpl__ @@ -0,0 +1,32 @@ +{ + "private": true, + "name": "<%= projectName %>", + "scripts": {}, + "type": "module", + "dependencies": { + "@remix-run/node": "<%= remixVersion %>", + "@remix-run/react": "<%= remixVersion %>", + "@remix-run/serve": "<%= remixVersion %>", + "isbot": "<%= isbotVersion %>", + "react": "<%= reactVersion %>", + "react-dom": "<%= reactDomVersion %>" + }, + "devDependencies": { + "@remix-run/dev": "<%= remixVersion %>", + "@types/react": "<%= typesReactVersion %>", + "@types/react-dom": "<%= typesReactDomVersion %>" + }, + "engines": { + "node": ">=20" + }, + "sideEffects": false<% if (isUsingTsSolution) { %>, + "nx": { + <%_ if (name !== projectName) { _%> + "name": "<%= name %>",<%_ } _%> + "projectType": "application", + "sourceRoot": "<%- projectRoot %>", + <%_ if (parsedTags?.length) { _%> + "tags": <%- JSON.stringify(parsedTags) %> + <%_ } _%> + }<% } %> +} diff --git a/packages/remix/src/generators/application/files/ts-solution/tsconfig.app.json__tmpl__ b/packages/remix/src/generators/application/files/ts-solution/tsconfig.app.json__tmpl__ new file mode 100644 index 0000000000000..7c24e92dadf98 --- /dev/null +++ b/packages/remix/src/generators/application/files/ts-solution/tsconfig.app.json__tmpl__ @@ -0,0 +1,39 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "app/**/*.ts", + "app/**/*.tsx", + "app/**/*.js", + "app/**/*.jsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "exclude": [ + "tests/**/*.spec.ts", + "tests/**/*.test.ts", + "tests/**/*.spec.tsx", + "tests/**/*.test.tsx", + "tests/**/*.spec.js", + "tests/**/*.test.js", + "tests/**/*.spec.jsx", + "tests/**/*.test.jsx" + ] +} diff --git a/packages/remix/src/generators/application/files/ts-solution/tsconfig.json__tmpl__ b/packages/remix/src/generators/application/files/ts-solution/tsconfig.json__tmpl__ new file mode 100644 index 0000000000000..de288cd7a6d9d --- /dev/null +++ b/packages/remix/src/generators/application/files/ts-solution/tsconfig.json__tmpl__ @@ -0,0 +1,10 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/packages/remix/src/generators/application/lib/add-e2e.ts b/packages/remix/src/generators/application/lib/add-e2e.ts index f496b1fd940a7..ee46632d59ccb 100644 --- a/packages/remix/src/generators/application/lib/add-e2e.ts +++ b/packages/remix/src/generators/application/lib/add-e2e.ts @@ -4,12 +4,14 @@ import { joinPathFragments, ensurePackage, readNxJson, + writeJson, } from '@nx/devkit'; import { type NormalizedSchema } from './normalize-options'; import { getPackageVersion } from '../../../utils/versions'; 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(tree: Tree, options: NormalizedSchema) { const hasRemixPlugin = readNxJson(tree).plugins?.find((p) => @@ -30,14 +32,31 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { typeof import('@nx/cypress') >('@nx/cypress', getPackageVersion(tree, 'nx')); - addProjectConfiguration(tree, options.e2eProjectName, { - projectType: 'application', - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - tags: [], - implicitDependencies: [options.projectName], - }); + 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, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + tags: [], + implicitDependencies: [options.projectName], + }); + } const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, @@ -93,14 +112,31 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) { typeof import('@nx/playwright') >('@nx/playwright', getPackageVersion(tree, 'nx')); - addProjectConfiguration(tree, options.e2eProjectName, { - projectType: 'application', - root: options.e2eProjectRoot, - sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), - targets: {}, - tags: [], - implicitDependencies: [options.projectName], - }); + 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, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + tags: [], + implicitDependencies: [options.projectName], + }); + } const e2eTask = await configurationGenerator(tree, { project: options.e2eProjectName, diff --git a/packages/remix/src/generators/application/schema.d.ts b/packages/remix/src/generators/application/schema.d.ts index ddecf258963eb..580a3fc855437 100644 --- a/packages/remix/src/generators/application/schema.d.ts +++ b/packages/remix/src/generators/application/schema.d.ts @@ -8,7 +8,10 @@ export interface NxRemixGeneratorSchema { unitTestRunner?: 'vitest' | 'jest' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'none'; skipFormat?: boolean; + // Internal options rootProject?: boolean; addPlugin?: boolean; nxCloudToken?: string; + useTsSolution?: boolean; + formatter?: 'prettier' | 'none'; } diff --git a/packages/remix/src/generators/application/schema.json b/packages/remix/src/generators/application/schema.json index 0a133441750ca..5b4ee76eee1cc 100644 --- a/packages/remix/src/generators/application/schema.json +++ b/packages/remix/src/generators/application/schema.json @@ -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?" }, diff --git a/packages/remix/src/generators/init/init.ts b/packages/remix/src/generators/init/init.ts index 470d6e9d879f8..c8695771c5741 100644 --- a/packages/remix/src/generators/init/init.ts +++ b/packages/remix/src/generators/init/init.ts @@ -1,17 +1,13 @@ import { - type Tree, + addDependenciesToPackageJson, + createProjectGraphAsync, formatFiles, GeneratorCallback, readNxJson, - addDependenciesToPackageJson, runTasksInSerial, - createProjectGraphAsync, + type Tree, } from '@nx/devkit'; -import { - addPlugin, - generateCombinations, -} from '@nx/devkit/src/utils/add-plugin'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; import { createNodesV2 } from '../../plugins/plugin'; import { nxVersion, remixVersion } from '../../utils/versions'; import { type Schema } from './schema'; @@ -21,8 +17,6 @@ export function remixInitGenerator(tree: Tree, options: Schema) { } export async function remixInitGeneratorInternal(tree: Tree, options: Schema) { - assertNotUsingTsSolutionSetup(tree, 'remix', 'init'); - const tasks: GeneratorCallback[] = []; if (!options.skipPackageJson) { diff --git a/packages/remix/src/generators/library/library.impl.ts b/packages/remix/src/generators/library/library.impl.ts index d08496214c424..1f0553763b160 100644 --- a/packages/remix/src/generators/library/library.impl.ts +++ b/packages/remix/src/generators/library/library.impl.ts @@ -1,7 +1,6 @@ import type { Tree } from '@nx/devkit'; import { formatFiles, GeneratorCallback, runTasksInSerial } from '@nx/devkit'; -import { Linter } from '@nx/eslint'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { initGenerator as jsInitGenerator } from '@nx/js'; import { libraryGenerator } from '@nx/react'; import { addTsconfigEntryPoints, @@ -10,6 +9,7 @@ import { updateBuildableConfig, } from './lib'; import type { NxRemixGeneratorSchema } from './schema'; +import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; export async function remixLibraryGenerator( tree: Tree, @@ -22,11 +22,15 @@ export async function remixLibraryGeneratorInternal( tree: Tree, schema: NxRemixGeneratorSchema ) { - assertNotUsingTsSolutionSetup(tree, 'remix', 'library'); - const tasks: GeneratorCallback[] = []; const options = await normalizeOptions(tree, schema); + const jsInitTask = await jsInitGenerator(tree, { + js: options.js, + skipFormat: true, + }); + tasks.push(jsInitTask); + const libGenTask = await libraryGenerator(tree, { name: options.projectName, style: options.style, @@ -36,9 +40,10 @@ export async function remixLibraryGeneratorInternal( directory: options.projectRoot, skipFormat: true, skipTsConfig: false, - linter: Linter.EsLint, + linter: options.linter, component: true, buildable: options.buildable, + bundler: options.bundler, addPlugin: options.addPlugin, }); tasks.push(libGenTask); @@ -50,10 +55,24 @@ export async function remixLibraryGeneratorInternal( addTsconfigEntryPoints(tree, options); - if (options.buildable) { + if (options.bundler === 'rollup' || options.buildable) { updateBuildableConfig(tree, options.projectName); } + updateTsconfigFiles( + tree, + 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(tree); } diff --git a/packages/remix/src/generators/library/schema.d.ts b/packages/remix/src/generators/library/schema.d.ts index 41283df98c462..a0c546e8fdc28 100644 --- a/packages/remix/src/generators/library/schema.d.ts +++ b/packages/remix/src/generators/library/schema.d.ts @@ -6,7 +6,10 @@ export interface NxRemixGeneratorSchema { style: SupportedStyles; tags?: string; importPath?: string; + /** @deprecated Use bundler instead. */ buildable?: boolean; + bundler?: 'none' | 'vite' | 'rollup'; + linter?: 'none' | 'eslint'; unitTestRunner?: 'jest' | 'vitest' | 'none'; js?: boolean; skipFormat?: boolean; diff --git a/packages/remix/src/generators/library/schema.json b/packages/remix/src/generators/library/schema.json index e2e9bae514ee0..d5c71fcad402b 100644 --- a/packages/remix/src/generators/library/schema.json +++ b/packages/remix/src/generators/library/schema.json @@ -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"] diff --git a/packages/rspack/src/executors/rspack/schema.json b/packages/rspack/src/executors/rspack/schema.json index 8a2ccb1ab5b53..0594e0dafef4a 100644 --- a/packages/rspack/src/executors/rspack/schema.json +++ b/packages/rspack/src/executors/rspack/schema.json @@ -97,7 +97,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.", diff --git a/packages/rspack/src/generators/application/application.ts b/packages/rspack/src/generators/application/application.ts index 60d01c7b85f14..77ae88c8680b6 100644 --- a/packages/rspack/src/generators/application/application.ts +++ b/packages/rspack/src/generators/application/application.ts @@ -1,5 +1,4 @@ import { ensurePackage, formatFiles, runTasksInSerial, Tree } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { version as nxVersion } from 'nx/package.json'; import configurationGenerator from '../configuration/configuration'; import rspackInitGenerator from '../init/init'; @@ -10,8 +9,6 @@ export default async function ( tree: Tree, _options: ApplicationGeneratorSchema ) { - assertNotUsingTsSolutionSetup(tree, 'rspack', 'application'); - const tasks = []; const initTask = await rspackInitGenerator(tree, { ..._options, diff --git a/packages/rspack/src/generators/configuration/configuration.ts b/packages/rspack/src/generators/configuration/configuration.ts index 53be9571c2272..4a977e450f320 100644 --- a/packages/rspack/src/generators/configuration/configuration.ts +++ b/packages/rspack/src/generators/configuration/configuration.ts @@ -5,7 +5,6 @@ import { readProjectConfiguration, Tree, } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { addOrChangeBuildTarget, addOrChangeServeTarget, @@ -26,8 +25,6 @@ export async function configurationGenerator( tree: Tree, options: ConfigurationSchema ) { - assertNotUsingTsSolutionSetup(tree, 'rspack', 'configuration'); - const task = await rspackInitGenerator(tree, { ...options, // TODO: Crystalize the default rspack.config.js file. diff --git a/packages/rspack/src/generators/init/init.ts b/packages/rspack/src/generators/init/init.ts index 6e43253f46ef0..f32af80d9c696 100644 --- a/packages/rspack/src/generators/init/init.ts +++ b/packages/rspack/src/generators/init/init.ts @@ -9,7 +9,6 @@ import { } from '@nx/devkit'; import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; import { initGenerator } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { createNodesV2 } from '../../../plugin'; import { lessLoaderVersion, @@ -25,8 +24,6 @@ export async function rspackInitGenerator( tree: Tree, schema: InitGeneratorSchema ) { - assertNotUsingTsSolutionSetup(tree, 'rspack', 'init'); - const tasks: GeneratorCallback[] = []; const nxJson = readNxJson(tree); diff --git a/packages/vite/src/generators/configuration/configuration.spec.ts b/packages/vite/src/generators/configuration/configuration.spec.ts index 7b3da15c89312..8fce12d571e47 100644 --- a/packages/vite/src/generators/configuration/configuration.spec.ts +++ b/packages/vite/src/generators/configuration/configuration.spec.ts @@ -1,6 +1,13 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; -import { addDependenciesToPackageJson, readJson, Tree } from '@nx/devkit'; +import { + addDependenciesToPackageJson, + addProjectConfiguration, + readJson, + Tree, + updateJson, + writeJson, +} from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { nxVersion } from '../../utils/versions'; @@ -35,6 +42,7 @@ describe('@nx/vite:configuration', () => { addPlugin: true, uiFramework: 'react', project: 'my-test-react-app', + projectType: 'application', }); }); @@ -321,4 +329,84 @@ describe('@nx/vite:configuration', () => { } ); }); + + describe('TS solution setup', () => { + beforeAll(async () => { + 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 create package.json with exports field for libraries', async () => { + addProjectConfiguration(tree, 'my-lib', { + root: 'packages/my-lib', + }); + writeJson(tree, 'packages/my-lib/tsconfig.lib.json', {}); + writeJson(tree, 'packages/my-lib/tsconfig.json', {}); + + await viteConfigurationGenerator(tree, { + addPlugin: true, + uiFramework: 'none', + project: 'my-lib', + projectType: 'library', + newProject: true, + }); + + expect(readJson(tree, 'packages/my-lib/package.json')) + .toMatchInlineSnapshot(` + { + "exports": { + ".": { + "default": "./dist/index.js", + "import": "./dist/index.mjs", + "types": "./dist/index.d.ts", + }, + "./package.json": "./package.json", + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "name": "@proj/my-lib", + "types": "./dist/index.d.ts", + "version": "0.0.1", + } + `); + }); + + it('should create package.json without exports field for apps', async () => { + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + }); + writeJson(tree, 'apps/my-app/tsconfig.app.json', {}); + writeJson(tree, 'apps/my-app/tsconfig.json', {}); + + await viteConfigurationGenerator(tree, { + addPlugin: true, + uiFramework: 'none', + project: 'my-app', + projectType: 'application', + newProject: true, + }); + + expect(readJson(tree, 'apps/my-app/package.json')).toMatchInlineSnapshot(` + { + "name": "@proj/my-app", + "private": true, + "version": "0.0.1", + } + `); + }); + }); }); diff --git a/packages/vite/src/generators/configuration/configuration.ts b/packages/vite/src/generators/configuration/configuration.ts index 57e8c502577ee..427a5303e873b 100644 --- a/packages/vite/src/generators/configuration/configuration.ts +++ b/packages/vite/src/generators/configuration/configuration.ts @@ -50,7 +50,8 @@ export async function viteConfigurationGeneratorInternal( const projectConfig = readProjectConfiguration(tree, schema.project); const { targets, root: projectRoot } = projectConfig; - const projectType = projectConfig.projectType ?? 'library'; + const projectType = + schema.projectType ?? projectConfig.projectType ?? 'library'; schema.includeLib ??= projectType === 'library'; @@ -161,12 +162,13 @@ export async function viteConfigurationGeneratorInternal( skipFormat: true, addPlugin: schema.addPlugin, compiler: schema.compiler, + projectType, }); tasks.push(vitestTask); } if (isUsingTsSolutionSetup(tree)) { - updatePackageJson(tree, schema); + updatePackageJson(tree, schema, projectType); } if (!schema.skipFormat) { @@ -180,7 +182,8 @@ export default viteConfigurationGenerator; function updatePackageJson( tree: Tree, - options: ViteConfigurationGeneratorSchema + options: ViteConfigurationGeneratorSchema, + projectType: 'application' | 'library' ) { const project = readProjectConfiguration(tree, options.project); @@ -193,28 +196,33 @@ function updatePackageJson( name: getImportPath(tree, options.project), version: '0.0.1', }; + if (projectType === 'application') { + packageJson.private = true; + } } - // we always write/override the vite and project config with some set values, - // so we can rely on them - const main = join(project.root, 'src/index.ts'); - // we configure the dts plugin with the entryRoot set to `src` - const rootDir = join(project.root, 'src'); - const outputPath = joinPathFragments(project.root, 'dist'); - - packageJson = getUpdatedPackageJsonContent(packageJson, { - main, - outputPath, - projectRoot: project.root, - rootDir, - generateExportsField: true, - packageJsonPath, - format: ['esm', 'cjs'], - // when building both formats, we don't set the package.json "type" field, so - // we need to set the esm extension to ".mjs" to match vite output - // see the "File Extensions" callout in https://vite.dev/guide/build.html#library-mode - outputFileExtensionForEsm: '.mjs', - }); + if (projectType === 'library') { + // we always write/override the vite and project config with some set values, + // so we can rely on them + const main = join(project.root, 'src/index.ts'); + // we configure the dts plugin with the entryRoot set to `src` + const rootDir = join(project.root, 'src'); + const outputPath = joinPathFragments(project.root, 'dist'); + + packageJson = getUpdatedPackageJsonContent(packageJson, { + main, + outputPath, + projectRoot: project.root, + rootDir, + generateExportsField: true, + packageJsonPath, + format: ['esm', 'cjs'], + // when building both formats, we don't set the package.json "type" field, so + // we need to set the esm extension to ".mjs" to match vite output + // see the "File Extensions" callout in https://vite.dev/guide/build.html#library-mode + outputFileExtensionForEsm: '.mjs', + }); + } writeJson(tree, packageJsonPath, packageJson); } diff --git a/packages/vite/src/generators/configuration/schema.d.ts b/packages/vite/src/generators/configuration/schema.d.ts index 907958eb2b28a..3b9d843556e40 100644 --- a/packages/vite/src/generators/configuration/schema.d.ts +++ b/packages/vite/src/generators/configuration/schema.d.ts @@ -8,5 +8,7 @@ export interface ViteConfigurationGeneratorSchema { includeLib?: boolean; skipFormat?: boolean; testEnvironment?: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string; + // Internal options addPlugin?: boolean; + projectType?: 'application' | 'library'; } diff --git a/packages/vite/src/generators/vitest/schema.d.ts b/packages/vite/src/generators/vitest/schema.d.ts index e876864ac4bac..8625e15530be5 100644 --- a/packages/vite/src/generators/vitest/schema.d.ts +++ b/packages/vite/src/generators/vitest/schema.d.ts @@ -10,4 +10,6 @@ export interface VitestGeneratorSchema { addPlugin?: boolean; runtimeTsconfigFileName?: string; compiler?: 'babel' | 'swc'; // default: babel + // internal options + projectType?: 'application' | 'library'; } diff --git a/packages/vite/src/generators/vitest/vitest-generator.ts b/packages/vite/src/generators/vitest/vitest-generator.ts index 1b03dd1e737e9..7082c01749cdf 100644 --- a/packages/vite/src/generators/vitest/vitest-generator.ts +++ b/packages/vite/src/generators/vitest/vitest-generator.ts @@ -51,7 +51,11 @@ export async function vitestGeneratorInternal( const tasks: GeneratorCallback[] = []; - const { root, projectType } = readProjectConfiguration(tree, schema.project); + const { root, projectType: _projectType } = readProjectConfiguration( + tree, + schema.project + ); + const projectType = schema.projectType ?? _projectType; const isRootProject = root === '.'; tasks.push(await jsInitGenerator(tree, { ...schema, skipFormat: true })); @@ -279,7 +283,9 @@ function createFiles( extendedConfig: isTsSolutionSetup ? `${rootOffset}tsconfig.base.json` : './tsconfig.json', - outDir: isTsSolutionSetup ? `./out-tsc/jest` : `${rootOffset}dist/out-tsc`, + outDir: isTsSolutionSetup + ? `./out-tsc/vitest` + : `${rootOffset}dist/out-tsc`, }); } diff --git a/packages/vite/src/plugins/plugin.spec.ts b/packages/vite/src/plugins/plugin.spec.ts index 1b3a9334f74b7..6eec64ff806a8 100644 --- a/packages/vite/src/plugins/plugin.spec.ts +++ b/packages/vite/src/plugins/plugin.spec.ts @@ -2,6 +2,7 @@ import { CreateNodesContext } from '@nx/devkit'; import { createNodesV2 } from './plugin'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { loadViteDynamicImport } from '../utils/executor-utils'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; jest.mock('../utils/executor-utils', () => ({ loadViteDynamicImport: jest.fn().mockResolvedValue({ @@ -9,6 +10,11 @@ jest.mock('../utils/executor-utils', () => ({ }), })); +jest.mock('@nx/js/src/utils/typescript/ts-solution-setup', () => ({ + ...jest.requireActual('@nx/js/src/utils/typescript/ts-solution-setup'), + isUsingTsSolutionSetup: jest.fn().mockReturnValue(false), +})); + describe('@nx/vite/plugin', () => { let createNodesFunction = createNodesV2[1]; let context: CreateNodesContext; @@ -87,6 +93,27 @@ describe('@nx/vite/plugin', () => { expect(targets?.['build-input']?.command).toMatch(/vite/); expect(targets?.['serve-input'].command).toMatch(/vite/); }); + + it('should infer typecheck with --build flag when using TS solution setup', async () => { + (isUsingTsSolutionSetup as jest.Mock).mockResolvedValue(true); + tempFs.createFileSync('tsconfig.json', ''); + + const nodes = await createNodesFunction( + ['vite.config.ts'], + { + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: 'test', + serveStaticTargetName: 'serve-static', + }, + context + ); + + expect(nodes[0][1].projects['.'].targets.typecheck.command).toEqual( + `tsc --build --emitDeclarationOnly --pretty --verbose` + ); + }); }); describe('not root project', () => { diff --git a/packages/vite/src/plugins/plugin.ts b/packages/vite/src/plugins/plugin.ts index 64868c07b2eb0..79eb7fed6ee9e 100644 --- a/packages/vite/src/plugins/plugin.ts +++ b/packages/vite/src/plugins/plugin.ts @@ -22,6 +22,7 @@ import { getLockFileName } from '@nx/js'; import { loadViteDynamicImport } from '../utils/executor-utils'; import { hashObject } from 'nx/src/hasher/file-hasher'; import { minimatch } from 'minimatch'; +import { isUsingTsSolutionSetup as _isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; const pmc = getPackageManagerCommand(); @@ -59,10 +60,17 @@ export const createNodesV2: CreateNodesV2 = [ const optionsHash = hashObject(options); const cachePath = join(workspaceDataDirectory, `vite-${optionsHash}.hash`); const targetsCache = readTargetsCache(cachePath); + const isUsingTsSolutionSetup = _isUsingTsSolutionSetup(); try { return await createNodesFromFiles( (configFile, options, context) => - createNodesInternal(configFile, options, context, targetsCache), + createNodesInternal( + configFile, + options, + context, + targetsCache, + isUsingTsSolutionSetup + ), configFilePaths, options, context @@ -79,7 +87,13 @@ export const createNodes: CreateNodes = [ logger.warn( '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' ); - return createNodesInternal(configFilePath, options, context, {}); + return createNodesInternal( + configFilePath, + options, + context, + {}, + _isUsingTsSolutionSetup() + ); }, ]; @@ -87,7 +101,8 @@ async function createNodesInternal( configFilePath: string, options: VitePluginOptions, context: CreateNodesContext, - targetsCache: Record + targetsCache: Record, + isUsingTsSolutionSetup: boolean ) { const projectRoot = dirname(configFilePath); // Do not create a project if package.json and project.json isn't there. @@ -119,6 +134,7 @@ async function createNodesInternal( projectRoot, normalizedOptions, tsConfigFiles, + isUsingTsSolutionSetup, context ); targetsCache[hash] ??= viteTargets; @@ -148,6 +164,7 @@ async function buildViteTargets( projectRoot: string, options: VitePluginOptions, tsConfigFiles: string[], + isUsingTsSolutionSetup: boolean, context: CreateNodesContext ): Promise { const absoluteConfigFilePath = joinPathFragments( @@ -218,7 +235,9 @@ async function buildViteTargets( : ['default', '^default']), { externalDependencies: ['typescript'] }, ], - command: `tsc --noEmit -p ${tsConfigToUse}`, + command: isUsingTsSolutionSetup + ? `tsc --build --emitDeclarationOnly --pretty --verbose` + : `tsc --noEmit -p ${tsConfigToUse}`, options: { cwd: joinPathFragments(projectRoot) }, metadata: { description: `Run Typechecking`, diff --git a/packages/webpack/src/generators/configuration/configuration.ts b/packages/webpack/src/generators/configuration/configuration.ts index e4b9de0c97224..f4cd67321e320 100644 --- a/packages/webpack/src/generators/configuration/configuration.ts +++ b/packages/webpack/src/generators/configuration/configuration.ts @@ -17,7 +17,6 @@ import { WebpackExecutorOptions } from '../../executors/webpack/schema'; import { hasPlugin } from '../../utils/has-plugin'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { ensureDependencies } from '../../utils/ensure-dependencies'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function configurationGenerator( tree: Tree, @@ -30,8 +29,6 @@ export async function configurationGeneratorInternal( tree: Tree, options: ConfigurationGeneratorSchema ) { - assertNotUsingTsSolutionSetup(tree, 'webpack', 'configuration'); - const tasks: GeneratorCallback[] = []; const nxJson = readNxJson(tree); const addPluginDefault = diff --git a/packages/webpack/src/generators/init/init.ts b/packages/webpack/src/generators/init/init.ts index ec19d0f2f1419..24f10417e6036 100644 --- a/packages/webpack/src/generators/init/init.ts +++ b/packages/webpack/src/generators/init/init.ts @@ -7,7 +7,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, webpackCliVersion } from '../../utils/versions'; import { Schema } from './schema'; @@ -17,8 +16,6 @@ export function webpackInitGenerator(tree: Tree, schema: Schema) { } export async function webpackInitGeneratorInternal(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'webpack', 'init'); - const nxJson = readNxJson(tree); const addPluginDefault = process.env.NX_ADD_PLUGINS !== 'false' && diff --git a/packages/workspace/src/generators/new/generate-preset.ts b/packages/workspace/src/generators/new/generate-preset.ts index dba0c37028ac1..6824da637cc09 100644 --- a/packages/workspace/src/generators/new/generate-preset.ts +++ b/packages/workspace/src/generators/new/generate-preset.ts @@ -85,6 +85,7 @@ export function generatePreset(host: Tree, opts: NormalizedSchema) { opts.prefix !== undefined ? `--prefix=${opts.prefix}` : null, opts.nxCloudToken ? `--nxCloudToken=${opts.nxCloudToken}` : null, opts.formatter ? `--formatter=${opts.formatter}` : null, + opts.workspaces ? `--workspaces` : null, ].filter((e) => !!e); } } diff --git a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts index 2a84d1791c92e..a53182eebf46f 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.spec.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.spec.ts @@ -1,4 +1,12 @@ -import { createNxCloudOnboardingURL } from 'nx/src/nx-cloud/utilities/url-shorten'; +import type { NxJsonConfiguration, Tree } from '@nx/devkit'; +import { formatFiles, readJson } from '@nx/devkit'; +import Ajv from 'ajv'; +import { generateWorkspaceFiles } from './generate-workspace-files'; +import { createTree } from '@nx/devkit/testing'; +import { Preset } from '../utils/presets'; +import * as nxSchema from 'nx/schemas/nx-schema.json'; +import { mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; jest.mock( 'nx/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud', @@ -18,16 +26,6 @@ jest.mock('nx/src/nx-cloud/utilities/url-shorten', () => ({ }, })); -import type { NxJsonConfiguration, Tree } from '@nx/devkit'; -import { formatFiles, readJson } from '@nx/devkit'; -import Ajv from 'ajv'; -import { generateWorkspaceFiles } from './generate-workspace-files'; -import { createTree } from '@nx/devkit/testing'; -import { Preset } from '../utils/presets'; -import * as nxSchema from 'nx/schemas/nx-schema.json'; -import { mkdirSync, writeFileSync } from 'fs'; -import { join } from 'path'; - describe('@nx/workspace:generateWorkspaceFiles', () => { let tree: Tree; @@ -232,7 +230,7 @@ describe('@nx/workspace:generateWorkspaceFiles', () => { "scripts": {}, "version": "0.0.0", "workspaces": [ - "packages/*", + "packages/**", ], } `); @@ -286,4 +284,57 @@ describe('@nx/workspace:generateWorkspaceFiles', () => { expect(readJson(tree, 'proj/package.json').scripts).toMatchSnapshot(); }); + + it('should create workspaces from workspaceGlobs (npm)', async () => { + tree.write('/proj/package.json', JSON.stringify({})); + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.NPM, + defaultBase: 'main', + packageManager: 'npm', + isCustomPreset: false, + workspaceGlobs: ['apps/**', 'packages/**'], + }); + + const packageJson = readJson(tree, '/proj/package.json'); + expect(packageJson).toMatchInlineSnapshot(` + { + "dependencies": {}, + "devDependencies": { + "nx": "0.0.1", + }, + "license": "MIT", + "name": "@proj/source", + "private": true, + "scripts": {}, + "version": "0.0.0", + "workspaces": [ + "apps/**", + "packages/**", + ], + } + `); + }); + + it('should create workspaces from workspaceGlobs (pnpm)', async () => { + tree.write('/proj/package.json', JSON.stringify({})); + await generateWorkspaceFiles(tree, { + name: 'proj', + directory: 'proj', + preset: Preset.NPM, + defaultBase: 'main', + packageManager: 'pnpm', + isCustomPreset: false, + workspaceGlobs: ['apps/**', 'packages/**'], + }); + + const packageJson = tree.read('/proj/pnpm-workspace.yaml', 'utf-8'); + expect(packageJson).toMatchInlineSnapshot(` + "packages: + - apps/** + - packages/** + " + `); + }); }); diff --git a/packages/workspace/src/generators/new/generate-workspace-files.ts b/packages/workspace/src/generators/new/generate-workspace-files.ts index 877ca6bcfcadc..da8bb75720680 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.ts @@ -416,18 +416,25 @@ function setUpWorkspacesInPackageJson(tree: Tree, options: NormalizedSchema) { options.preset === Preset.NPM || (options.preset === Preset.TS && process.env.NX_ADD_PLUGINS !== 'false' && - process.env.NX_ADD_TS_PLUGIN !== 'false') + process.env.NX_ADD_TS_PLUGIN !== 'false') || + ((options.preset === Preset.Expo || + options.preset === Preset.NextJs || + options.preset === Preset.ReactMonorepo || + options.preset === Preset.ReactNative || + options.preset === Preset.RemixMonorepo) && + options.workspaces) ) { + const workspaces = options.workspaceGlobs ?? ['packages/**']; if (options.packageManager === 'pnpm') { tree.write( join(options.directory, 'pnpm-workspace.yaml'), - `packages: - - 'packages/*' + `packages: + - ${workspaces.join('\n - ')} ` ); } else { updateJson(tree, join(options.directory, 'package.json'), (json) => { - json.workspaces = ['packages/*']; + json.workspaces = workspaces; return json; }); } diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index b0ce463c6df18..0167a358e11ed 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -38,12 +38,15 @@ interface Schema { useGitHub?: boolean; nxCloud?: 'yes' | 'skip' | 'circleci' | 'github'; formatter?: 'none' | 'prettier'; + workspaces?: boolean; + workspaceGlobs?: string | string[]; } export interface NormalizedSchema extends Schema { presetVersion?: string; isCustomPreset: boolean; nxCloudToken?: string; + workspaceGlobs?: string[]; } export async function newGenerator(tree: Tree, opts: Schema) { @@ -136,6 +139,11 @@ function parsePresetName(input: string): { package: string; version?: string } { function normalizeOptions(options: Schema): NormalizedSchema { const normalized: Partial = { ...options, + workspaceGlobs: Array.isArray(options.workspaceGlobs) + ? options.workspaceGlobs + : options.workspaceGlobs + ? [options.workspaceGlobs] + : undefined, }; if (!options.directory) { diff --git a/packages/workspace/src/generators/new/schema.json b/packages/workspace/src/generators/new/schema.json index 4f895ab3a8fda..65c62cc936c4e 100644 --- a/packages/workspace/src/generators/new/schema.json +++ b/packages/workspace/src/generators/new/schema.json @@ -50,7 +50,7 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], + "enum": ["eslint", "none"], "default": "eslint" }, "packageManager": { @@ -92,6 +92,11 @@ "type": "string", "enum": ["none", "prettier"], "default": "none" + }, + "workspaces": { + "description": "Whether to use package manager workspaces.", + "type": "boolean", + "default": false } }, "additionalProperties": true diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index ad115d754021b..3b9a8ba5331fe 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -71,6 +71,8 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'playwright', addPlugin, nxCloudToken: options.nxCloudToken, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.ReactStandalone) { const { applicationGenerator: reactApplicationGenerator } = require('@nx' + @@ -87,6 +89,7 @@ async function createPreset(tree: Tree, options: Schema) { unitTestRunner: options.bundler === 'vite' ? 'vitest' : 'jest', addPlugin, nxCloudToken: options.nxCloudToken, + formatter: options.formatter, }); } else if (options.preset === Preset.RemixMonorepo) { const { applicationGenerator: remixApplicationGenerator } = require('@nx' + @@ -100,6 +103,8 @@ async function createPreset(tree: Tree, options: Schema) { unitTestRunner: 'vitest', addPlugin, nxCloudToken: options.nxCloudToken, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.RemixStandalone) { const { applicationGenerator: remixApplicationGenerator } = require('@nx' + @@ -114,6 +119,7 @@ async function createPreset(tree: Tree, options: Schema) { unitTestRunner: 'vitest', addPlugin, nxCloudToken: options.nxCloudToken, + formatter: options.formatter, }); } else if (options.preset === Preset.VueMonorepo) { const { applicationGenerator: vueApplicationGenerator } = require('@nx' + @@ -184,6 +190,8 @@ async function createPreset(tree: Tree, options: Schema) { src: options.nextSrcDir, e2eTestRunner: options.e2eTestRunner ?? 'playwright', addPlugin, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.NextJsStandalone) { const { applicationGenerator: nextApplicationGenerator } = require('@nx' + @@ -198,6 +206,7 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'playwright', rootProject: true, addPlugin, + formatter: options.formatter, }); } else if (options.preset === Preset.WebComponents) { const { applicationGenerator: webApplicationGenerator } = require('@nx' + @@ -246,6 +255,8 @@ async function createPreset(tree: Tree, options: Schema) { addPlugin, nxCloudToken: options.nxCloudToken, bundler: options.bundler ?? 'webpack', + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.Expo) { const { expoApplicationGenerator } = require('@nx' + '/expo'); @@ -256,6 +267,8 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'detox', addPlugin, nxCloudToken: options.nxCloudToken, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.TS) { const { initGenerator } = require('@nx' + '/js'); diff --git a/packages/workspace/src/generators/preset/schema.d.ts b/packages/workspace/src/generators/preset/schema.d.ts index 8c9d19f54c1ed..e25c2b9084b8e 100644 --- a/packages/workspace/src/generators/preset/schema.d.ts +++ b/packages/workspace/src/generators/preset/schema.d.ts @@ -7,6 +7,7 @@ export interface Schema { style?: string; linter?: string; formatter?: 'none' | 'prettier'; + workspaces?: boolean; standaloneConfig?: boolean; framework?: string; packageManager?: PackageManager; diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index 891e099699022..53f305180dfb9 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -17,7 +17,7 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], + "enum": ["eslint", "none"], "default": "eslint" }, "routing": { @@ -103,6 +103,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"]