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

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

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

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

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


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

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

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

## Related Issue(s)
#28322

---------

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,17 +56,29 @@
] ]
} }
}, },
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",
"enum": ["eslint"], "enum": ["eslint", "none"],
"default": "eslint" "default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["vitest", "jest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.", "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": { "tags": {
"type": "string", "type": "string",
@ -99,7 +111,8 @@
"buildable": { "buildable": {
"type": "boolean", "type": "boolean",
"default": false, "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": { "importPath": {
"type": "string", "type": "string",

View File

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

View File

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

View File

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

View File

@ -74,12 +74,6 @@
] ]
} }
}, },
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"routing": { "routing": {
"type": "boolean", "type": "boolean",
"description": "Generate application with routes.", "description": "Generate application with routes.",
@ -98,11 +92,29 @@
"default": false, "default": false,
"x-priority": "internal" "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": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["vitest", "jest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.", "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": { "inSourceTests": {
"type": "boolean", "type": "boolean",
@ -165,14 +177,6 @@
"default": false, "default": false,
"hidden": true "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": { "minimal": {
"description": "Generate a React app with a minimal setup, no separate test files.", "description": "Generate a React app with a minimal setup, no separate test files.",
"type": "boolean", "type": "boolean",

View File

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

View File

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

View File

@ -37,17 +37,29 @@
"enum": ["none", "css"], "enum": ["none", "css"],
"default": "css" "default": "css"
}, },
"buildable": { "bundler": {
"type": "boolean", "type": "string",
"description": "Should the library be buildable?", "description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"default": false "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": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["vitest", "jest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test Runner to use for Unit Tests", "description": "Test Runner to use for Unit Tests",
"x-prompt": "What test runner should be used?", "x-prompt": "What test runner should be used?",
"default": "vitest" "default": "none",
"x-priority": "important"
}, },
"importPath": { "importPath": {
"type": "string", "type": "string",
@ -63,6 +75,12 @@
"description": "Skip formatting files after generator runs", "description": "Skip formatting files after generator runs",
"default": false, "default": false,
"x-priority": "internal" "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"], "required": ["directory"],

View File

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

View File

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

View File

@ -17,7 +17,7 @@
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",
"enum": ["eslint"], "enum": ["eslint", "none"],
"default": "eslint" "default": "eslint"
}, },
"routing": { "routing": {
@ -100,6 +100,17 @@
"prefix": { "prefix": {
"description": "The prefix to use for Angular component and directive selectors.", "description": "The prefix to use for Angular component and directive selectors.",
"type": "string" "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"], "required": ["preset", "name"],

View File

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

View File

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

View File

@ -40,10 +40,10 @@ describe('@nx/expo (legacy)', () => {
return nxJson; return nxJson;
}); });
runCLI( 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( 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(() => { afterAll(() => {
@ -210,7 +210,9 @@ describe('@nx/expo (legacy)', () => {
const appName = uniq('app1'); const appName = uniq('app1');
const libName = uniq('@my-org/lib1'); 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 // check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided // 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}` `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 // check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided // 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 () => { it('should run e2e for playwright', async () => {
const appName2 = uniq('my-app'); const appName2 = uniq('my-app');
runCLI( 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()) { if (runE2ETests()) {
const results = runCLI(`e2e ${appName2}-e2e`, { verbose: true }); const results = runCLI(`e2e ${appName2}-e2e`, { verbose: true });

View File

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

View File

@ -10,7 +10,7 @@ describe('Jest root projects', () => {
packages: ['@nx/angular'], packages: ['@nx/angular'],
}); });
runCLI( 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); }, 300_000);
it('should add lib project and tests should still work', async () => { 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 ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow(); expect(() => runCLI(`test ${myapp}`)).not.toThrow();
@ -32,7 +34,7 @@ describe('Jest root projects', () => {
packages: ['@nx/react'], packages: ['@nx/react'],
}); });
runCLI( 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); }, 300_000);
it('should add lib project and tests should still work', async () => { 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 ${mylib}`)).not.toThrow();
expect(() => runCLI(`test ${myapp}`)).not.toThrow(); expect(() => runCLI(`test ${myapp}`)).not.toThrow();

View File

@ -32,7 +32,7 @@ describe('Next.js Styles', () => {
const lessApp = uniq('app'); const lessApp = uniq('app');
runCLI( 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, { await checkApp(lessApp, {
@ -44,7 +44,7 @@ describe('Next.js Styles', () => {
const scApp = uniq('app'); const scApp = uniq('app');
runCLI( 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, { await checkApp(scApp, {
@ -56,7 +56,7 @@ describe('Next.js Styles', () => {
const scAppWithAppRouter = uniq('app'); const scAppWithAppRouter = uniq('app');
runCLI( 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, { await checkApp(scAppWithAppRouter, {
@ -68,7 +68,7 @@ describe('Next.js Styles', () => {
const emotionApp = uniq('app'); const emotionApp = uniq('app');
runCLI( 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, { await checkApp(emotionApp, {
@ -83,7 +83,7 @@ describe('Next.js Styles', () => {
const tailwindApp = uniq('app'); const tailwindApp = uniq('app');
runCLI( 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, { await checkApp(tailwindApp, {
@ -107,7 +107,7 @@ describe('Next.js Styles', () => {
const tailwindApp = uniq('app'); const tailwindApp = uniq('app');
runCLI( 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, { await checkApp(tailwindApp, {

View File

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

View File

@ -19,7 +19,7 @@ describe('@nx/workspace:convert-to-monorepo', () => {
it('should convert a standalone webpack and jest react project to a monorepo (legacy)', async () => { it('should convert a standalone webpack and jest react project to a monorepo (legacy)', async () => {
const reactApp = uniq('reactapp'); const reactApp = uniq('reactapp');
runCLI( 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: { env: {
NX_ADD_PLUGINS: 'false', NX_ADD_PLUGINS: 'false',

View File

@ -32,7 +32,7 @@ describe('@nx/workspace:infer-targets', () => {
// default case, everything is generated with crystal, everything should be skipped // default case, everything is generated with crystal, everything should be skipped
const remixApp = uniq('remix'); const remixApp = uniq('remix');
runCLI( 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`); 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 // default case, everything is generated with crystal, relevant plugins should be skipped
const remixApp = uniq('remix'); const remixApp = uniq('remix');
runCLI( 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( 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 // even if we make sure there are executors for remix & remix-e2e, only remix conversions will run with --project option
const remixApp = uniq('remix'); const remixApp = uniq('remix');
runCLI( 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) => { 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 () => { it('should be convert a standalone vite and playwright react project to a monorepo', async () => {
const reactApp = uniq('reactapp'); const reactApp = uniq('reactapp');
runCLI( 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'); runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive');

View File

@ -39,10 +39,10 @@ describe('@nx/react-native (legacy)', () => {
return nxJson; return nxJson;
}); });
runCLI( 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( 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(() => { afterAll(() => {
@ -265,7 +265,7 @@ describe('@nx/react-native (legacy)', () => {
const libName = uniq('@my-org/lib1'); const libName = uniq('@my-org/lib1');
runCLI( 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 // check files are generated without the layout directory ("apps/") and
@ -274,7 +274,9 @@ describe('@nx/react-native (legacy)', () => {
// check tests pass // check tests pass
expect(() => runCLI(`test ${appName}`)).not.toThrow(); 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 // check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided // 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 () => { it('should run build with vite bundler and e2e with playwright', async () => {
const appName2 = uniq('my-app'); const appName2 = uniq('my-app');
runCLI( 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(); expect(() => runCLI(`build ${appName2}`)).not.toThrow();
if (runE2ETests()) { if (runE2ETests()) {

View File

@ -18,7 +18,7 @@ describe('@nx/react-native', () => {
newProject(); newProject();
appName = uniq('app'); appName = uniq('app');
runCLI( 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 () => { it('should run build with vite bundler and e2e with playwright', async () => {
const appName2 = uniq('my-app'); const appName2 = uniq('my-app');
runCLI( 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(); expect(() => runCLI(`build ${appName2}`)).not.toThrow();
if (runE2ETests()) { if (runE2ETests()) {

View File

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

View File

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

View File

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

View File

@ -67,9 +67,9 @@ describe('rspack e2e', () => {
// Make sure expected files are present. // Make sure expected files are present.
/** /**
* The files that are generated are: * 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}`); result = runCLI(`test ${project}`);
expect(result).toContain('Successfully ran target test'); expect(result).toContain('Successfully ran target test');
@ -87,7 +87,7 @@ describe('rspack e2e', () => {
env: { NODE_ENV: 'production' }, env: { NODE_ENV: 'production' },
}); });
expect(result).toContain('Successfully ran target build'); 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 // Generate a new app and check that the files are correct
const app2 = uniq('app2'); const app2 = uniq('app2');
@ -120,7 +120,7 @@ describe('rspack e2e', () => {
}); });
expect(result).toContain('Successfully ran target build'); expect(result).toContain('Successfully ran target build');
// Make sure expected files are present. // Make sure expected files are present.
expect(listFiles(`dist/${app2}`)).toHaveLength(6); expect(listFiles(`dist/${app2}`)).toHaveLength(5);
result = runCLI(`test ${app2}`); result = runCLI(`test ${app2}`);
expect(result).toContain('Successfully ran target test'); expect(result).toContain('Successfully ran target test');
@ -139,11 +139,11 @@ describe('rspack e2e', () => {
result = runCLI(`build ${app3}`); result = runCLI(`build ${app3}`);
expect(result).toContain('Successfully ran target build'); expect(result).toContain('Successfully ran target build');
// Make sure expected files are present. // Make sure expected files are present.
expect(listFiles(`dist/${app3}`)).toHaveLength(3); expect(listFiles(`dist/${app3}`)).toHaveLength(2);
result = runCLI(`build ${app3} --generatePackageJson=true`); result = runCLI(`build ${app3} --generatePackageJson=true`);
expect(result).toContain('Successfully ran target build'); expect(result).toContain('Successfully ran target build');
// Make sure expected files are present. // Make sure expected files are present.
expect(listFiles(`dist/${app3}`)).toHaveLength(5); expect(listFiles(`dist/${app3}`)).toHaveLength(4);
}, 200_000); }, 200_000);
}); });

View File

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

View File

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

View File

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

View File

@ -52,6 +52,9 @@ interface ReactArguments extends BaseArguments {
nextAppDir: boolean; nextAppDir: boolean;
nextSrcDir: boolean; nextSrcDir: boolean;
e2eTestRunner: 'none' | 'cypress' | 'playwright'; e2eTestRunner: 'none' | 'cypress' | 'playwright';
linter?: 'none' | 'eslint';
formatter?: 'none' | 'prettier';
workspaces?: boolean;
} }
interface AngularArguments extends BaseArguments { interface AngularArguments extends BaseArguments {
@ -157,6 +160,15 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
describe: chalk.dim`Bundler to be used to build the app.`, describe: chalk.dim`Bundler to be used to build the app.`,
type: 'string', 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', { .option('framework', {
describe: chalk.dim`Framework option to be used with certain stacks.`, describe: chalk.dim`Framework option to be used with certain stacks.`,
type: 'string', 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( async function determineNoneOptions(
parsedArgs: yargs.Arguments<NoneArguments> parsedArgs: yargs.Arguments<NoneArguments>
): Promise<Partial<NoneArguments>> { ): Promise<Partial<NoneArguments>> {
@ -448,26 +506,9 @@ async function determineNoneOptions(
process.env.NX_ADD_PLUGINS !== 'false' && process.env.NX_ADD_PLUGINS !== 'false' &&
process.env.NX_ADD_TS_PLUGIN !== '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 { return {
preset: Preset.TS, preset: Preset.TS,
formatter: reply.prettier === 'Yes' ? 'prettier' : 'none', formatter: await determineFormatterOptions(parsedArgs),
}; };
} else { } else {
let preset: Preset; let preset: Preset;
@ -535,6 +576,10 @@ async function determineReactOptions(
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined; let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
let nextAppDir = false; let nextAppDir = false;
let nextSrcDir = 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) { if (parsedArgs.preset && parsedArgs.preset !== Preset.React) {
preset = parsedArgs.preset; preset = parsedArgs.preset;
@ -550,27 +595,25 @@ async function determineReactOptions(
} else { } else {
const framework = await determineReactFramework(parsedArgs); const framework = await determineReactFramework(parsedArgs);
// React Native and Expo only support integrated monorepos for now. const isStandalone =
// TODO(jack): Add standalone support for React Native and Expo. workspaces || framework === 'react-native' || framework === 'expo'
const workspaceType = ? false
framework === 'react-native' || framework === 'expo' : (await determineStandaloneOrMonorepo()) === 'standalone';
? 'integrated'
: await determineStandaloneOrMonorepo();
if (workspaceType === 'standalone') { if (isStandalone) {
appName = parsedArgs.name; appName = parsedArgs.name;
} else { } else {
appName = await determineAppName(parsedArgs); appName = await determineAppName(parsedArgs);
} }
if (framework === 'nextjs') { if (framework === 'nextjs') {
if (workspaceType === 'standalone') { if (isStandalone) {
preset = Preset.NextJsStandalone; preset = Preset.NextJsStandalone;
} else { } else {
preset = Preset.NextJs; preset = Preset.NextJs;
} }
} else if (framework === 'remix') { } else if (framework === 'remix') {
if (workspaceType === 'standalone') { if (isStandalone) {
preset = Preset.RemixStandalone; preset = Preset.RemixStandalone;
} else { } else {
preset = Preset.RemixMonorepo; preset = Preset.RemixMonorepo;
@ -580,7 +623,7 @@ async function determineReactOptions(
} else if (framework === 'expo') { } else if (framework === 'expo') {
preset = Preset.Expo; preset = Preset.Expo;
} else { } else {
if (workspaceType === 'standalone') { if (isStandalone) {
preset = Preset.ReactStandalone; preset = Preset.ReactStandalone;
} else { } else {
preset = Preset.ReactMonorepo; preset = Preset.ReactMonorepo;
@ -657,6 +700,14 @@ async function determineReactOptions(
style = reply.style; style = reply.style;
} }
if (workspaces) {
linter = await determineLinterOptions(parsedArgs);
formatter = await determineFormatterOptions(parsedArgs);
} else {
linter = 'eslint';
formatter = 'prettier';
}
return { return {
preset, preset,
style, style,
@ -665,6 +716,9 @@ async function determineReactOptions(
nextAppDir, nextAppDir,
nextSrcDir, nextSrcDir,
e2eTestRunner, e2eTestRunner,
linter,
formatter,
workspaces,
}; };
} }

View File

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

View File

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

View File

@ -105,6 +105,26 @@ export async function configurationGeneratorInternal(
addTarget(tree, opts, projectGraph); 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 { root: projectRoot } = readProjectConfiguration(tree, options.project);
const isTsSolutionSetup = isUsingTsSolutionSetup(tree); const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
if (isTsSolutionSetup) { if (isTsSolutionSetup) {
@ -201,6 +221,7 @@ In this case you need to provide a devServerTarget,'<projectName>:<targetName>[:
return { return {
...options, ...options,
bundler: options.bundler ?? 'webpack', bundler: options.bundler ?? 'webpack',
projectRoot: projectConfig.root,
rootProject: options.rootProject ?? projectConfig.root === '.', rootProject: options.rootProject ?? projectConfig.root === '.',
linter, linter,
devServerTarget, devServerTarget,
@ -408,6 +429,9 @@ function createPackageJson(tree: Tree, options: NormalizedSchema) {
name: importPath, name: importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: {
name: options.project,
},
}; };
writeJson(tree, packageJsonPath, packageJson); writeJson(tree, packageJsonPath, packageJson);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import {
Tree, Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js'; 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 { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest'; import { addJest } from '../../utils/add-jest';
@ -36,16 +36,16 @@ export async function expoApplicationGeneratorInternal(
host: Tree, host: Tree,
schema: Schema schema: Schema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'expo', 'application');
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, { const jsInitTask = await jsInitGenerator(host, {
...schema, ...schema,
skipFormat: true, skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
}); });
const options = await normalizeOptions(host, schema);
tasks.push(jsInitTask); tasks.push(jsInitTask);
const initTask = await initGenerator(host, { ...options, skipFormat: true }); const initTask = await initGenerator(host, { ...options, skipFormat: true });
tasks.push(initTask); tasks.push(initTask);
@ -80,6 +80,21 @@ export async function expoApplicationGeneratorInternal(
tasks.push(e2eTask); tasks.push(e2eTask);
addEasScripts(host); 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }

View File

@ -1,10 +1,11 @@
import { GeneratorCallback, Tree } from '@nx/devkit';
import { import {
addProjectConfiguration, addProjectConfiguration,
ensurePackage, ensurePackage,
getPackageManagerCommand, GeneratorCallback,
joinPathFragments, joinPathFragments,
readNxJson, readNxJson,
Tree,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web'; 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 { addE2eCiTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { findPluginForConfigFile } from '@nx/devkit/src/utils/find-plugin-for-config-file'; 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 { 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( export async function addE2e(
tree: Tree, tree: Tree,
@ -40,14 +42,31 @@ export async function addE2e(
typeof import('@nx/cypress') typeof import('@nx/cypress')
>('@nx/cypress', nxVersion); >('@nx/cypress', nxVersion);
addProjectConfiguration(tree, options.e2eProjectName, { if (isUsingTsSolutionSetup(tree)) {
projectType: 'application', writeJson(
root: options.e2eProjectRoot, tree,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), joinPathFragments(options.e2eProjectRoot, 'package.json'),
targets: {}, {
implicitDependencies: [options.projectName], name: options.e2eProjectName,
tags: [], 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, { const e2eTask = await configurationGenerator(tree, {
...options, ...options,
@ -106,13 +125,30 @@ export async function addE2e(
const { configurationGenerator } = ensurePackage< const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright') typeof import('@nx/playwright')
>('@nx/playwright', nxVersion); >('@nx/playwright', nxVersion);
addProjectConfiguration(tree, options.e2eProjectName, { if (isUsingTsSolutionSetup(tree)) {
projectType: 'application', writeJson(
root: options.e2eProjectRoot, tree,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), joinPathFragments(options.e2eProjectRoot, 'package.json'),
targets: {}, {
implicitDependencies: [options.projectName], 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, { const e2eTask = await configurationGenerator(tree, {
project: options.e2eProjectName, project: options.e2eProjectName,

View File

@ -1,14 +1,18 @@
import { import {
addProjectConfiguration, addProjectConfiguration,
joinPathFragments,
ProjectConfiguration, ProjectConfiguration,
readNxJson, readNxJson,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { hasExpoPlugin } from '../../../utils/has-expo-plugin'; import { hasExpoPlugin } from '../../../utils/has-expo-plugin';
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; 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) { export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host); const nxJson = readNxJson(host);
@ -26,12 +30,28 @@ export function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags, tags: options.parsedTags,
}; };
addProjectConfiguration( if (isUsingTsSolutionSetup(host)) {
host, const packageName = getImportPath(host, options.name);
options.projectName, writeJson(host, joinPathFragments(options.appProjectRoot, 'package.json'), {
projectConfiguration, name: packageName,
options.standaloneConfig 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) { function getTargets(options: NormalizedSchema) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import {
formatFiles, formatFiles,
generateFiles, generateFiles,
GeneratorCallback, GeneratorCallback,
installPackagesTask,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot, offsetFromRoot,
@ -13,6 +14,7 @@ import {
Tree, Tree,
updateJson, updateJson,
updateProjectConfiguration, updateProjectConfiguration,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
@ -20,7 +22,6 @@ import {
getRelativePathToRootTsConfig, getRelativePathToRootTsConfig,
initGenerator as jsInitGenerator, initGenerator as jsInitGenerator,
} from '@nx/js'; } from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import init from '../init/init'; import init from '../init/init';
import { addLinting } from '../../utils/add-linting'; import { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest'; 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 { initRootBabelConfig } from '../../utils/init-root-babel-config';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; 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( export async function expoLibraryGenerator(
host: Tree, host: Tree,
@ -50,7 +53,13 @@ export async function expoLibraryGeneratorInternal(
host: Tree, host: Tree,
schema: Schema schema: Schema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'expo', 'library'); const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
if (options.publishable === true && !schema.importPath) { 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 }); const initTask = await init(host, { ...options, skipFormat: true });
tasks.push(initTask); tasks.push(initTask);
if (!options.skipPackageJson) { 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }
// Always run install to link packages.
if (options.isUsingTsSolutionConfig) {
tasks.push(() => installPackagesTask(host));
}
tasks.push(() => { tasks.push(() => {
logShowProjectCommand(options.name); logShowProjectCommand(options.name);
}); });
@ -136,7 +157,29 @@ async function addProject(
tags: options.parsedTags, tags: options.parsedTags,
targets: {}, 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) { if (!options.publishable && !options.buildable) {
return () => {}; return () => {};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -268,7 +268,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
`); `);
}); });
it('should not invoke tsc with `--emitDeclarationOnly` when `noEmit` is set in the tsconfig.json file', async () => { it('should not invoke `tsc --build` when `noEmit` is set in the tsconfig.json file', async () => {
// set directly in tsconfig.json file // set directly in tsconfig.json file
await applyFilesToTempFsAndContext(tempFs, context, { await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({ 'libs/my-lib/tsconfig.json': JSON.stringify({
@ -285,7 +285,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"targets": { "targets": {
"typecheck": { "typecheck": {
"cache": true, "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": [ "dependsOn": [
"^typecheck", "^typecheck",
], ],
@ -345,7 +345,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"targets": { "targets": {
"typecheck": { "typecheck": {
"cache": true, "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": [ "dependsOn": [
"^typecheck", "^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, { await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': JSON.stringify({ 'libs/my-lib/tsconfig.json': JSON.stringify({
files: [], files: [],
@ -407,7 +407,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"targets": { "targets": {
"typecheck": { "typecheck": {
"cache": true, "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": [ "dependsOn": [
"^typecheck", "^typecheck",
], ],

View File

@ -168,12 +168,23 @@ async function createNodesInternal(
return {}; 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: * The cache key is composed by:
* - hashes of the content of the relevant files that can affect what's inferred by the plugin: * - hashes of the content of the relevant files that can affect what's inferred by the plugin:
* - current config file * - current config file
* - config files extended by the current config file (recursively up to the root 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 * - lock file
* - hash of the plugin options * - hash of the plugin options
* - current config file path * - current config file path
@ -185,11 +196,17 @@ async function createNodesInternal(
context.workspaceRoot, context.workspaceRoot,
projectRoot projectRoot
); );
const externalProjectReferences = resolveShallowExternalProjectReferences(
tsConfig,
context.workspaceRoot,
projectRoot
);
const nodeHash = hashArray([ const nodeHash = hashArray([
...[ ...[
fullConfigPath, fullConfigPath,
...extendedConfigFiles.files, ...extendedConfigFiles.files,
...Object.keys(internalReferencedFiles), ...Object.keys(internalReferencedFiles),
...Object.keys(externalProjectReferences),
join(context.workspaceRoot, lockFileName), join(context.workspaceRoot, lockFileName),
].map(hashFile), ].map(hashFile),
hashObject(options), hashObject(options),
@ -239,6 +256,11 @@ function buildTscTargets(
context.workspaceRoot, context.workspaceRoot,
projectRoot projectRoot
); );
const externalProjectReferences = resolveShallowExternalProjectReferences(
tsConfig,
context.workspaceRoot,
projectRoot
);
const targetName = options.typecheck.targetName; const targetName = options.typecheck.targetName;
if (!targets[targetName]) { if (!targets[targetName]) {
let command = `tsc --build --emitDeclarationOnly --pretty --verbose`; let command = `tsc --build --emitDeclarationOnly --pretty --verbose`;
@ -246,11 +268,13 @@ function buildTscTargets(
tsConfig.options.noEmit || tsConfig.options.noEmit ||
Object.values(internalProjectReferences).some( Object.values(internalProjectReferences).some(
(ref) => ref.options.noEmit (ref) => ref.options.noEmit
) ||
Object.values(externalProjectReferences).some(
(ref) => ref.options.noEmit
) )
) { ) {
// `--emitDeclarationOnly` and `--noEmit` are mutually exclusive, so // `tsc --build` does not work with `noEmit: true`
// we remove `--emitDeclarationOnly` if `--noEmit` is set. 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."`;
command = `tsc --build --pretty --verbose`;
} }
targets[targetName] = { targets[targetName] = {
@ -607,6 +631,48 @@ function resolveInternalProjectReferences(
workspaceRoot: string, workspaceRoot: string,
projectRoot: string, projectRoot: string,
projectReferences: Record<string, ParsedCommandLine> = {} projectReferences: Record<string, ParsedCommandLine> = {}
): Record<string, ParsedCommandLine> {
walkProjectReferences(
tsConfig,
workspaceRoot,
projectRoot,
(configPath, config) => {
if (isExternalProjectReference(configPath, workspaceRoot, projectRoot)) {
return false;
} else {
projectReferences[configPath] = config;
}
}
);
return projectReferences;
}
function resolveShallowExternalProjectReferences(
tsConfig: ParsedCommandLine,
workspaceRoot: string,
projectRoot: string,
projectReferences: Record<string, ParsedCommandLine> = {}
): Record<string, ParsedCommandLine> {
walkProjectReferences(
tsConfig,
workspaceRoot,
projectRoot,
(configPath, config) => {
if (isExternalProjectReference(configPath, workspaceRoot, projectRoot)) {
projectReferences[configPath] = config;
}
return false;
}
);
return projectReferences;
}
function walkProjectReferences(
tsConfig: ParsedCommandLine,
workspaceRoot: string,
projectRoot: string,
visitor: (configPath: string, config: ParsedCommandLine) => void | false, // false stops recursion
projectReferences: Record<string, ParsedCommandLine> = {}
): Record<string, ParsedCommandLine> { ): Record<string, ParsedCommandLine> {
if (!tsConfig.projectReferences?.length) { if (!tsConfig.projectReferences?.length) {
return projectReferences; return projectReferences;
@ -624,22 +690,14 @@ function resolveInternalProjectReferences(
continue; continue;
} }
if (isExternalProjectReference(refConfigPath, workspaceRoot, projectRoot)) {
continue;
}
if (!refConfigPath.endsWith('.json')) { if (!refConfigPath.endsWith('.json')) {
refConfigPath = join(refConfigPath, 'tsconfig.json'); refConfigPath = join(refConfigPath, 'tsconfig.json');
} }
const refTsConfig = readCachedTsConfig(refConfigPath); const refTsConfig = readCachedTsConfig(refConfigPath);
projectReferences[refConfigPath] = refTsConfig; const result = visitor(refConfigPath, refTsConfig);
if (result !== false) {
resolveInternalProjectReferences( walkProjectReferences(refTsConfig, workspaceRoot, projectRoot, visitor);
refTsConfig, }
workspaceRoot,
projectRoot,
projectReferences
);
} }
return projectReferences; return projectReferences;

View File

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

View File

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

View File

@ -5,6 +5,8 @@ import {
readNxJson, readNxJson,
readProjectConfiguration, readProjectConfiguration,
Tree, Tree,
updateJson,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { Schema } from './schema'; import { Schema } from './schema';
@ -174,23 +176,22 @@ describe('app', () => {
describe('--style scss', () => { describe('--style scss', () => {
it('should generate scss styles', async () => { it('should generate scss styles', async () => {
const name = uniq();
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: name, directory: 'myapp',
style: 'scss', style: 'scss',
}); });
expect(tree.exists(`${name}/src/app/page.module.scss`)).toBeTruthy(); expect(tree.exists(`myapp/src/app/page.module.scss`)).toBeTruthy();
expect(tree.exists(`${name}/src/app/global.css`)).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(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(` .toMatchInlineSnapshot(`
"import './global.css'; "import './global.css';
export const metadata = { export const metadata = {
title: 'Welcome to ${name}', title: 'Welcome to myapp',
description: 'Generated by create-nx-workspace', description: 'Generated by create-nx-workspace',
}; };
@ -212,23 +213,22 @@ describe('app', () => {
describe('--style less', () => { describe('--style less', () => {
it('should generate less styles', async () => { it('should generate less styles', async () => {
const name = uniq();
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: name, directory: 'myapp',
style: 'less', style: 'less',
}); });
expect(tree.exists(`${name}/src/app/page.module.less`)).toBeTruthy(); expect(tree.exists(`myapp/src/app/page.module.less`)).toBeTruthy();
expect(tree.exists(`${name}/src/app/global.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(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(` .toMatchInlineSnapshot(`
"import './global.less'; "import './global.less';
export const metadata = { export const metadata = {
title: 'Welcome to ${name}', title: 'Welcome to myapp',
description: 'Generated by create-nx-workspace', description: 'Generated by create-nx-workspace',
}; };
@ -616,10 +616,8 @@ describe('app', () => {
}); });
it('should add .eslintrc.json and dependencies', async () => { it('should add .eslintrc.json and dependencies', async () => {
const name = uniq();
await applicationGenerator(tree, { await applicationGenerator(tree, {
directory: name, directory: 'myapp',
style: 'css', 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(` expect(eslintJson).toMatchInlineSnapshot(`
{ {
"extends": [ "extends": [
@ -655,7 +653,7 @@ describe('app', () => {
"rules": { "rules": {
"@next/next/no-html-link-for-pages": [ "@next/next/no-html-link-for-pages": [
"error", "error",
"${name}/pages", "myapp/pages",
], ],
}, },
}, },
@ -838,6 +836,172 @@ describe('app (legacy)', () => {
expect(projectConfiguration.targets.build).toBeDefined(); expect(projectConfiguration.targets.build).toBeDefined();
expect(projectConfiguration.targets.serve).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() { function uniq() {

View File

@ -7,7 +7,6 @@ import {
Tree, Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js'; import { initGenerator as jsInitGenerator } from '@nx/js';
import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { setupTailwindGenerator } from '@nx/react'; import { setupTailwindGenerator } from '@nx/react';
import { import {
testingLibraryReactVersion, testingLibraryReactVersion,
@ -31,6 +30,7 @@ import { updateCypressTsConfig } from './lib/update-cypress-tsconfig';
import { showPossibleWarnings } from './lib/show-possible-warnings'; import { showPossibleWarnings } from './lib/show-possible-warnings';
import { tsLibVersion } from '../../utils/versions'; import { tsLibVersion } from '../../utils/versions';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; 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) { export async function applicationGenerator(host: Tree, schema: Schema) {
return await applicationGeneratorInternal(host, { return await applicationGeneratorInternal(host, {
@ -40,8 +40,6 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
} }
export async function applicationGeneratorInternal(host: Tree, schema: Schema) { export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
assertNotUsingTsSolutionSetup(host, 'next', 'application');
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const options = await normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
@ -51,6 +49,8 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
js: options.js, js: options.js,
skipPackageJson: options.skipPackageJson, skipPackageJson: options.skipPackageJson,
skipFormat: true, skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
}); });
tasks.push(jsInitTask); 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,13 +9,13 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { libraryGenerator as reactLibraryGenerator } from '@nx/react/src/generators/library/library'; import { libraryGenerator as reactLibraryGenerator } from '@nx/react/src/generators/library/library';
import { addTsConfigPath, initGenerator as jsInitGenerator } from '@nx/js'; 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 { testingLibraryReactVersion } from '@nx/react/src/utils/versions';
import { nextInitGenerator } from '../init/init'; import { nextInitGenerator } from '../init/init';
import { Schema } from './schema'; import { Schema } from './schema';
import { normalizeOptions } from './lib/normalize-options'; import { normalizeOptions } from './lib/normalize-options';
import { eslintConfigNextVersion, tsLibVersion } from '../../utils/versions'; 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) { export async function libraryGenerator(host: Tree, rawOptions: Schema) {
return await libraryGeneratorInternal(host, { return await libraryGeneratorInternal(host, {
@ -25,8 +25,6 @@ export async function libraryGenerator(host: Tree, rawOptions: Schema) {
} }
export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) { export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) {
assertNotUsingTsSolutionSetup(host, 'next', 'library');
const options = await normalizeOptions(host, rawOptions); const options = await normalizeOptions(host, rawOptions);
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
@ -45,7 +43,7 @@ export async function libraryGeneratorInternal(host: Tree, rawOptions: Schema) {
const libTask = await reactLibraryGenerator(host, { const libTask = await reactLibraryGenerator(host, {
...options, ...options,
compiler: 'swc', bundler: 'none',
skipFormat: true, skipFormat: true,
}); });
tasks.push(libTask); 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }

View File

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

View File

@ -62,17 +62,29 @@
] ]
} }
}, },
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite", "rollup"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"linter": { "linter": {
"description": "The tool to use for running lint checks.", "description": "The tool to use for running lint checks.",
"type": "string", "type": "string",
"enum": ["eslint"], "enum": ["eslint", "none"],
"default": "eslint" "default": "none",
"x-prompt": "Which linter would you like to use?",
"x-priority": "important"
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["vitest", "jest", "none"], "enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.", "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": { "tags": {
"type": "string", "type": "string",
@ -105,7 +117,8 @@
"buildable": { "buildable": {
"type": "boolean", "type": "boolean",
"default": false, "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": { "importPath": {
"type": "string", "type": "string",

View File

@ -95,12 +95,17 @@ export async function configurationGeneratorInternal(
}; };
if (isTsSolutionSetup) { 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.outDir = 'dist';
tsconfig.compilerOptions.tsBuildInfoFile = 'dist/tsconfig.tsbuildinfo'; tsconfig.compilerOptions.tsBuildInfoFile = 'dist/tsconfig.tsbuildinfo';
if (!options.rootProject) { if (!options.rootProject) {
// add the project tsconfog to the workspace root tsconfig.json references
updateJson(tree, 'tsconfig.json', (json) => { updateJson(tree, 'tsconfig.json', (json) => {
// add the project tsconfig to the workspace root tsconfig.json references
json.references ??= []; json.references ??= [];
json.references.push({ path: './' + projectConfig.root }); json.references.push({ path: './' + projectConfig.root });
return json; return json;
@ -130,6 +135,9 @@ export async function configurationGeneratorInternal(
name: importPath, name: importPath,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: {
name: options.project,
},
}; };
writeJson(tree, packageJsonPath, packageJson); writeJson(tree, packageJsonPath, packageJson);
} }

View File

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

View File

@ -9,7 +9,6 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js'; import { initGenerator as jsInitGenerator } from '@nx/js';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; 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 { addLinting } from '../../utils/add-linting';
import { addJest } from '../../utils/add-jest'; import { addJest } from '../../utils/add-jest';
@ -26,6 +25,7 @@ import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies'; import { ensureDependencies } from '../../utils/ensure-dependencies';
import { syncDeps } from '../../executors/sync-deps/sync-deps.impl'; import { syncDeps } from '../../executors/sync-deps/sync-deps.impl';
import { PackageJson } from 'nx/src/utils/package-json'; import { PackageJson } from 'nx/src/utils/package-json';
import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup';
export async function reactNativeApplicationGenerator( export async function reactNativeApplicationGenerator(
host: Tree, host: Tree,
@ -41,16 +41,16 @@ export async function reactNativeApplicationGeneratorInternal(
host: Tree, host: Tree,
schema: Schema schema: Schema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'react-native', 'application');
const options = await normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, { const jsInitTask = await jsInitGenerator(host, {
...schema, ...schema,
skipFormat: true, skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
}); });
tasks.push(jsInitTask); tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema);
const initTask = await initGenerator(host, { ...options, skipFormat: true }); const initTask = await initGenerator(host, { ...options, skipFormat: true });
tasks.push(initTask); 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }

View File

@ -1,11 +1,15 @@
import { import {
addProjectConfiguration, addProjectConfiguration,
joinPathFragments,
ProjectConfiguration, ProjectConfiguration,
readNxJson, readNxJson,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { NormalizedSchema } from './normalize-options'; 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) { export function addProject(host: Tree, options: NormalizedSchema) {
const nxJson = readNxJson(host); const nxJson = readNxJson(host);
@ -23,9 +27,24 @@ export function addProject(host: Tree, options: NormalizedSchema) {
tags: options.parsedTags, tags: options.parsedTags,
}; };
addProjectConfiguration(host, options.projectName, { if (isUsingTsSolutionSetup(host)) {
...project, 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) { function getTargets(options: NormalizedSchema) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import {
formatFiles, formatFiles,
generateFiles, generateFiles,
GeneratorCallback, GeneratorCallback,
installPackagesTask,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot, offsetFromRoot,
@ -13,6 +14,7 @@ import {
Tree, Tree,
updateJson, updateJson,
updateProjectConfiguration, updateProjectConfiguration,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
@ -32,7 +34,11 @@ import { NormalizedSchema, normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema'; import { Schema } from './schema';
import { ensureDependencies } from '../../utils/ensure-dependencies'; import { ensureDependencies } from '../../utils/ensure-dependencies';
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; 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( export async function reactNativeLibraryGenerator(
host: Tree, host: Tree,
@ -48,7 +54,13 @@ export async function reactNativeLibraryGeneratorInternal(
host: Tree, host: Tree,
schema: Schema schema: Schema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'react-native', 'library'); const tasks: GeneratorCallback[] = [];
const jsInitTask = await jsInitGenerator(host, {
...schema,
skipFormat: true,
});
tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema); const options = await normalizeOptions(host, schema);
if (options.publishable === true && !schema.importPath) { 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 }); const initTask = await init(host, { ...options, skipFormat: true });
tasks.push(initTask); 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }
// Always run install to link packages.
if (options.isUsingTsSolutionConfig) {
tasks.push(() => installPackagesTask(host));
}
tasks.push(() => { tasks.push(() => {
logShowProjectCommand(options.name); logShowProjectCommand(options.name);
}); });
@ -135,7 +158,27 @@ async function addProject(
targets: {}, 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) { if (!options.publishable && !options.buildable) {
return () => {}; return () => {};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,14 +21,9 @@ import {
Tree, Tree,
updateNxJson, updateNxJson,
} from '@nx/devkit'; } from '@nx/devkit';
import reactInitGenerator from '../init/init'; import reactInitGenerator from '../init/init';
import { Linter, lintProjectGenerator } from '@nx/eslint'; import { Linter, lintProjectGenerator } from '@nx/eslint';
import { import { babelLoaderVersion, nxVersion } from '../../utils/versions';
babelLoaderVersion,
nxRspackVersion,
nxVersion,
} from '../../utils/versions';
import { maybeJs } from '../../utils/maybe-js'; import { maybeJs } from '../../utils/maybe-js';
import { installCommonDependencies } from './lib/install-common-dependencies'; import { installCommonDependencies } from './lib/install-common-dependencies';
import { extractTsConfigBase } from '../../utils/create-ts-config'; 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 { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind'; import { setupTailwindGenerator } from '../setup-tailwind/setup-tailwind';
import { useFlatConfig } from '@nx/eslint/src/utils/flat-config'; 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) { async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
@ -114,20 +109,20 @@ export async function applicationGeneratorInternal(
host: Tree, host: Tree,
schema: Schema schema: Schema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
assertNotUsingTsSolutionSetup(host, 'react', 'application');
const tasks = []; const tasks = [];
const options = await normalizeOptions(host, schema);
showPossibleWarnings(host, options);
const jsInitTask = await jsInitGenerator(host, { const jsInitTask = await jsInitGenerator(host, {
...schema, ...schema,
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
skipFormat: true, skipFormat: true,
addTsPlugin: schema.useTsSolution,
formatter: schema.formatter,
}); });
tasks.push(jsInitTask); tasks.push(jsInitTask);
const options = await normalizeOptions(host, schema);
showPossibleWarnings(host, options);
const initTask = await reactInitGenerator(host, { const initTask = await reactInitGenerator(host, {
...options, ...options,
skipFormat: true, skipFormat: true,
@ -165,10 +160,7 @@ export async function applicationGeneratorInternal(
tasks.push(ensureDependencies(host, { uiFramework: 'react' })); tasks.push(ensureDependencies(host, { uiFramework: 'react' }));
} }
} else if (options.bundler === 'rspack') { } else if (options.bundler === 'rspack') {
const { rspackInitGenerator } = ensurePackage( const { rspackInitGenerator } = ensurePackage('@nx/rspack', nxVersion);
'@nx/rspack',
nxRspackVersion
);
const rspackInitTask = await rspackInitGenerator(host, { const rspackInitTask = await rspackInitGenerator(host, {
...options, ...options,
addPlugin: false, addPlugin: false,
@ -213,6 +205,7 @@ export async function applicationGeneratorInternal(
compiler: options.compiler, compiler: options.compiler,
skipFormat: true, skipFormat: true,
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
projectType: 'application',
}); });
tasks.push(viteTask); tasks.push(viteTask);
createOrEditViteConfig( createOrEditViteConfig(
@ -236,6 +229,26 @@ export async function applicationGeneratorInternal(
}, },
false 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') { 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) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,12 @@ import {
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { maybeJs } from '../../../utils/maybe-js'; import { maybeJs } from '../../../utils/maybe-js';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin'; import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration = { const project: ProjectConfiguration = {
@ -36,9 +38,25 @@ export function addProject(host: Tree, options: NormalizedSchema) {
}; };
} }
addProjectConfiguration(host, options.projectName, { if (options.isUsingTsSolutionConfig) {
...project, 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( function createRspackBuildTarget(

View File

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

View File

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

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