From a8de7df0e0507305d74c3b31e59ec5321c7d6da8 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 12 Dec 2024 15:43:14 -0500 Subject: [PATCH] feat(js): update vue/node app and lib generators to support TS solutions (#29299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --------- Co-authored-by: Leosvel PĂ©rez Espinosa --- .../express/generators/application.json | 9 +- .../packages/nest/generators/application.json | 7 +- .../packages/nest/generators/library.json | 8 +- .../packages/node/generators/application.json | 24 +- .../packages/node/generators/library.json | 12 +- .../packages/nuxt/generators/application.json | 17 +- .../packages/vue/generators/application.json | 17 +- .../packages/vue/generators/library.json | 8 +- e2e/eslint/src/linter.test.ts | 2 +- e2e/jest/src/jest.test.ts | 2 +- e2e/node/src/node-esbuild.test.ts | 2 +- e2e/node/src/node-server.test.ts | 22 +- e2e/node/src/node-ts-solution.test.ts | 170 ++++++++++++++ e2e/node/src/node-webpack.test.ts | 2 +- e2e/node/src/node.test.ts | 74 ++++--- e2e/nuxt/src/nuxt.test.ts | 2 +- e2e/nx/src/run.test.ts | 2 +- e2e/utils/create-project-utils.ts | 4 +- e2e/vue/src/vue-ts-solution.test.ts | 75 +++++++ .../bin/create-nx-workspace.ts | 52 ++++- packages/express/package.json | 1 - .../application/application.spec.ts | 176 ++++++++++++++- .../src/generators/application/application.ts | 3 - .../src/generators/application/schema.json | 9 +- packages/express/src/generators/init/init.ts | 3 - .../application/application.spec.ts | 172 +++++++++++++- .../src/generators/application/application.ts | 3 - .../src/generators/application/schema.d.ts | 1 + .../src/generators/application/schema.json | 7 +- packages/nest/src/generators/init/init.ts | 3 - .../src/generators/library/library.spec.ts | 112 +++++++++- .../nest/src/generators/library/library.ts | 3 - .../nest/src/generators/library/schema.json | 8 +- .../application/application.spec.ts | 160 ++++++++++++++ .../src/generators/application/application.ts | 80 +++++-- .../src/generators/application/schema.d.ts | 2 + .../src/generators/application/schema.json | 24 +- .../e2e-project/e2e-project.spec.ts | 90 +++++++- .../src/generators/e2e-project/e2e-project.ts | 102 +++++++-- .../files/cli/jest.config.ts__tmpl__ | 2 +- .../files/cli/tsconfig.spec.json__tmpl__ | 12 - .../tsconfig.json__tmpl__ | 0 .../tsconfig.spec.json__tmpl__ | 0 .../server/common/jest.config.ts__tmpl__ | 2 +- .../files/server/common/tsconfig.json__tmpl__ | 13 -- .../files/ts-solution/tsconfig.json__tmpl__ | 16 ++ packages/node/src/generators/init/init.ts | 3 - .../library/files/lib/package.json__tmpl__ | 4 - .../tsconfig.lib.json | 0 .../files/ts-solution/tsconfig.lib.json | 15 ++ .../src/generators/library/library.spec.ts | 163 +++++++++++++- .../node/src/generators/library/library.ts | 105 ++++++--- .../node/src/generators/library/schema.json | 12 +- .../__snapshots__/application.spec.ts.snap | 4 - .../application/application.spec.ts | 177 ++++++++++++++- .../src/generators/application/application.ts | 66 ++++-- .../application/lib/normalize-options.ts | 2 + .../src/generators/application/schema.d.ts | 3 + .../src/generators/application/schema.json | 17 +- packages/nuxt/src/generators/init/init.ts | 3 - packages/nuxt/src/utils/create-ts-config.ts | 4 +- .../generators/configuration/configuration.ts | 7 +- .../application/application.spec.ts | 2 + .../src/generators/library/library.spec.ts | 2 +- .../application/application.impl.spec.ts | 2 + packages/vite/src/utils/generator-utils.ts | 9 +- .../__snapshots__/application.spec.ts.snap | 1 + .../application/application.spec.ts | 185 ++++++++++++++++ .../src/generators/application/application.ts | 74 +++++-- .../files/common/src/vue-shims.d.ts.template | 5 + .../application/lib/normalize-options.ts | 2 + .../src/generators/application/schema.d.ts | 3 + .../src/generators/application/schema.json | 17 +- packages/vue/src/generators/init/init.ts | 3 - .../library/files/package.json__tmpl__ | 12 - .../library/lib/create-library-files.ts | 19 +- .../library/lib/normalize-options.ts | 2 + .../src/generators/library/library.spec.ts | 209 ++++++++++++++++++ .../vue/src/generators/library/library.ts | 92 ++++++-- .../vue/src/generators/library/schema.d.ts | 1 + .../vue/src/generators/library/schema.json | 8 +- packages/vue/src/utils/create-ts-config.ts | 73 ++++++ .../new/generate-workspace-files.ts | 6 +- .../workspace/src/generators/preset/preset.ts | 10 + 84 files changed, 2512 insertions(+), 325 deletions(-) create mode 100644 e2e/node/src/node-ts-solution.test.ts create mode 100644 e2e/vue/src/vue-ts-solution.test.ts delete mode 100644 packages/node/src/generators/e2e-project/files/cli/tsconfig.spec.json__tmpl__ rename packages/node/src/generators/e2e-project/files/{cli => non-ts-solution}/tsconfig.json__tmpl__ (100%) rename packages/node/src/generators/e2e-project/files/{server/common => non-ts-solution}/tsconfig.spec.json__tmpl__ (100%) delete mode 100644 packages/node/src/generators/e2e-project/files/server/common/tsconfig.json__tmpl__ create mode 100644 packages/node/src/generators/e2e-project/files/ts-solution/tsconfig.json__tmpl__ delete mode 100644 packages/node/src/generators/library/files/lib/package.json__tmpl__ rename packages/node/src/generators/library/files/{lib => non-ts-solution}/tsconfig.lib.json (100%) create mode 100644 packages/node/src/generators/library/files/ts-solution/tsconfig.lib.json create mode 100644 packages/vue/src/generators/application/files/common/src/vue-shims.d.ts.template delete mode 100644 packages/vue/src/generators/library/files/package.json__tmpl__ diff --git a/docs/generated/packages/express/generators/application.json b/docs/generated/packages/express/generators/application.json index eacbd36bd225f..78107a36585de 100644 --- a/docs/generated/packages/express/generators/application.json +++ b/docs/generated/packages/express/generators/application.json @@ -33,14 +33,17 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?" }, "tags": { "type": "string", diff --git a/docs/generated/packages/nest/generators/application.json b/docs/generated/packages/nest/generators/application.json index c77f46a1aac85..27826e6cfe037 100644 --- a/docs/generated/packages/nest/generators/application.json +++ b/docs/generated/packages/nest/generators/application.json @@ -37,13 +37,16 @@ "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], - "default": "eslint" + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "description": "Test runner to use for unit tests.", "type": "string", "enum": ["jest", "none"], - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?" }, "e2eTestRunner": { "type": "string", diff --git a/docs/generated/packages/nest/generators/library.json b/docs/generated/packages/nest/generators/library.json index f8e967fa0818d..9a98b70016afd 100644 --- a/docs/generated/packages/nest/generators/library.json +++ b/docs/generated/packages/nest/generators/library.json @@ -31,13 +31,17 @@ "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], - "default": "eslint" + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "description": "Test runner to use for unit tests.", "type": "string", "enum": ["jest", "none"], - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?", + "x-priority": "important" }, "tags": { "description": "Add tags to the library (used for linting).", diff --git a/docs/generated/packages/node/generators/application.json b/docs/generated/packages/node/generators/application.json index 903ead6c595e6..6aa298ea4903d 100644 --- a/docs/generated/packages/node/generators/application.json +++ b/docs/generated/packages/node/generators/application.json @@ -36,14 +36,26 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "default": "none", + "x-priority": "important", + "x-prompt": "Which unit test runner would you like to use?" + }, + "e2eTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for end-to-end tests", + "default": "none", + "x-priority": "important", + "x-prompt": "Which end-to-end test runner would you like to use?" }, "tags": { "type": "string", @@ -109,12 +121,6 @@ "hidden": true, "x-priority": "internal" }, - "e2eTestRunner": { - "type": "string", - "enum": ["jest", "none"], - "description": "Test runner to use for end to end (e2e) tests", - "default": "jest" - }, "docker": { "type": "boolean", "description": "Add a docker build target" diff --git a/docs/generated/packages/node/generators/library.json b/docs/generated/packages/node/generators/library.json index fada543625113..f6b9ab4d739a0 100644 --- a/docs/generated/packages/node/generators/library.json +++ b/docs/generated/packages/node/generators/library.json @@ -36,14 +36,18 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?", + "x-priority": "important" }, "tags": { "type": "string", @@ -69,7 +73,7 @@ }, "buildable": { "type": "boolean", - "default": false, + "default": true, "description": "Generate a buildable library.", "x-priority": "important" }, diff --git a/docs/generated/packages/nuxt/generators/application.json b/docs/generated/packages/nuxt/generators/application.json index 16e17b76a4f83..4877abbc54106 100644 --- a/docs/generated/packages/nuxt/generators/application.json +++ b/docs/generated/packages/nuxt/generators/application.json @@ -22,24 +22,27 @@ "pattern": "^[a-zA-Z][^:]*$", "x-priority": "important" }, - "linter": { - "description": "The tool to use for running lint checks.", - "type": "string", - "enum": ["eslint"], - "default": "eslint" - }, "skipFormat": { "description": "Skip formatting files.", "type": "boolean", "default": false, "x-priority": "internal" }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" + }, "unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "description": "Test runner to use for unit tests.", "x-prompt": "Which unit test runner would you like to use?", - "default": "none" + "default": "none", + "x-priority": "important" }, "e2eTestRunner": { "type": "string", diff --git a/docs/generated/packages/vue/generators/application.json b/docs/generated/packages/vue/generators/application.json index 70212ea56f8e9..d545250bec98a 100644 --- a/docs/generated/packages/vue/generators/application.json +++ b/docs/generated/packages/vue/generators/application.json @@ -54,12 +54,6 @@ ] } }, - "linter": { - "description": "The tool to use for running lint checks.", - "type": "string", - "enum": ["eslint", "none"], - "default": "eslint" - }, "routing": { "type": "boolean", "description": "Generate application with routes.", @@ -72,12 +66,21 @@ "default": false, "x-priority": "internal" }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" + }, "unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "description": "Test runner to use for unit tests.", "x-prompt": "Which unit test runner would you like to use?", - "default": "vitest" + "default": "none", + "x-priority": "important" }, "inSourceTests": { "type": "boolean", diff --git a/docs/generated/packages/vue/generators/library.json b/docs/generated/packages/vue/generators/library.json index 664b2b97a1307..418e359c36a42 100644 --- a/docs/generated/packages/vue/generators/library.json +++ b/docs/generated/packages/vue/generators/library.json @@ -36,13 +36,17 @@ "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], - "default": "eslint" + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "description": "Test runner to use for unit tests.", - "x-prompt": "What unit test runner should be used?" + "x-prompt": "What unit test runner should be used?", + "default": "none", + "x-priority": "important" }, "inSourceTests": { "type": "boolean", diff --git a/e2e/eslint/src/linter.test.ts b/e2e/eslint/src/linter.test.ts index 67fe89ec353e5..2a3c1faa1e580 100644 --- a/e2e/eslint/src/linter.test.ts +++ b/e2e/eslint/src/linter.test.ts @@ -772,7 +772,7 @@ describe('Linter', () => { const mylib = uniq('mylib'); runCLI( - `generate @nx/node:app --name=${myapp} --linter eslint --directory="." --no-interactive` + `generate @nx/node:app --name=${myapp} --linter=eslint --directory="." --e2eTestRunner=jest --no-interactive` ); runCLI('reset', { env: { CI: 'false' } }); verifySuccessfulStandaloneSetup(myapp); diff --git a/e2e/jest/src/jest.test.ts b/e2e/jest/src/jest.test.ts index afb824d03a757..eebf07802fddd 100644 --- a/e2e/jest/src/jest.test.ts +++ b/e2e/jest/src/jest.test.ts @@ -141,7 +141,7 @@ describe('Jest', () => { it('should be able to test node lib with babel-jest', async () => { const libName = uniq('babel-test-lib'); runCLI( - `generate @nx/node:lib libs/${libName} --buildable --importPath=@some-org/babel-test --publishable --babelJest` + `generate @nx/node:lib libs/${libName} --buildable --importPath=@some-org/babel-test --publishable --babelJest --unitTestRunner=jest` ); const cliResults = await runCLIAsync(`test ${libName}`); diff --git a/e2e/node/src/node-esbuild.test.ts b/e2e/node/src/node-esbuild.test.ts index e3531b48dcf4a..5b9da308d0bf5 100644 --- a/e2e/node/src/node-esbuild.test.ts +++ b/e2e/node/src/node-esbuild.test.ts @@ -24,7 +24,7 @@ describe('Node Applications + esbuild', () => { const app = uniq('nodeapp'); runCLI( - `generate @nx/node:app apps/${app} --bundler=esbuild --no-interactive` + `generate @nx/node:app apps/${app} --bundler=esbuild --no-interactive --linter=eslint --unitTestRunner=jest` ); checkFilesDoNotExist(`apps/${app}/webpack.config.js`); diff --git a/e2e/node/src/node-server.test.ts b/e2e/node/src/node-server.test.ts index cf5361fdebb8c..af5a72f995bcd 100644 --- a/e2e/node/src/node-server.test.ts +++ b/e2e/node/src/node-server.test.ts @@ -80,19 +80,23 @@ describe('Node Applications + webpack', () => { const nestApp = uniq('nest'); beforeAll(() => { - runCLI(`generate @nx/node:lib libs/${testLib1}`); - runCLI(`generate @nx/node:lib libs/${testLib2} --importPath=@acme/test2`); runCLI( - `generate @nx/node:app apps/${expressApp} --framework=express --port=7000 --no-interactive` + `generate @nx/node:lib libs/${testLib1} --linter=eslint --unitTestRunner=jest --buildable=false` ); runCLI( - `generate @nx/node:app apps/${fastifyApp} --framework=fastify --port=7001 --no-interactive` + `generate @nx/node:lib libs/${testLib2} --importPath=@acme/test2 --linter=eslint --unitTestRunner=jest --buildable=false` ); runCLI( - `generate @nx/node:app apps/${koaApp} --framework=koa --port=7002 --no-interactive` + `generate @nx/node:app apps/${expressApp} --framework=express --port=7000 --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` ); runCLI( - `generate @nx/node:app apps/${nestApp} --framework=nest --port=7003 --bundler=webpack --no-interactive` + `generate @nx/node:app apps/${fastifyApp} --framework=fastify --port=7001 --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` + ); + runCLI( + `generate @nx/node:app apps/${koaApp} --framework=koa --port=7002 --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` + ); + runCLI( + `generate @nx/node:app apps/${nestApp} --framework=nest --port=7003 --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest` ); addLibImport(expressApp, testLib1); @@ -165,7 +169,7 @@ describe('Node Applications + webpack', () => { const expressApp = 'docker-express-app'; // needs to be consistent for the Dockerfile snapshot runCLI( - `generate @nx/node:app apps/${expressApp} --framework=express --docker --no-interactive` + `generate @nx/node:app apps/${expressApp} --framework=express --docker --no-interactive --linter=eslint --unitTestRunner=jest` ); checkFilesExist(`apps/${expressApp}/Dockerfile`); @@ -179,10 +183,10 @@ describe('Node Applications + webpack', () => { // Set ports to avoid conflicts with other tests that might run in parallel runCLI( - `generate @nx/node:app apps/${nodeApp1} --framework=none --no-interactive --port=4444` + `generate @nx/node:app apps/${nodeApp1} --framework=none --no-interactive --port=4444 --linter=eslint --unitTestRunner=jest` ); runCLI( - `generate @nx/node:app apps/${nodeApp2} --framework=none --no-interactive --port=4445` + `generate @nx/node:app apps/${nodeApp2} --framework=none --no-interactive --port=4445 --linter=eslint --unitTestRunner=jest` ); updateJson(join('apps', nodeApp1, 'project.json'), (config) => { config.targets.serve.options.waitUntilTargets = [`${nodeApp2}:build`]; diff --git a/e2e/node/src/node-ts-solution.test.ts b/e2e/node/src/node-ts-solution.test.ts new file mode 100644 index 0000000000000..424cccffaedda --- /dev/null +++ b/e2e/node/src/node-ts-solution.test.ts @@ -0,0 +1,170 @@ +import { + checkFilesExist, + cleanupProject, + getPackageManagerCommand, + getSelectedPackageManager, + killPorts, + newProject, + promisifiedTreeKill, + runCLI, + runCommand, + runCommandUntil, + tmpProjPath, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; +import { execSync } from 'child_process'; +import * as http from 'http'; + +let originalEnvPort; + +describe('Node Applications', () => { + const pm = getSelectedPackageManager(); + + beforeAll(() => { + originalEnvPort = process.env.PORT; + newProject({ + packages: ['@nx/node', '@nx/express', '@nx/nest', '@nx/webpack'], + preset: 'ts', + }); + if (pm === 'pnpm') { + updateFile( + 'pnpm-workspace.yaml', + ` +packages: + - 'apps/**' + - 'packages/**' +` + ); + } else { + updateJson('package.json', (json) => { + json.workspaces = ['apps/**', 'packages/**']; + return json; + }); + } + }); + + afterAll(() => { + process.env.PORT = originalEnvPort; + cleanupProject(); + }); + + it('should be able to generate an empty application', async () => { + const nodeapp = uniq('nodeapp'); + const port = getRandomPort(); + process.env.PORT = `${port}`; + runCLI( + `generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest` + ); + + expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow(); + expect(() => runCLI(`test ${nodeapp}`)).not.toThrow(); + + updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`); + runCLI(`build ${nodeapp}`); + + checkFilesExist(`dist/apps/${nodeapp}/main.js`); + const result = execSync(`node dist/apps/${nodeapp}/main.js`, { + cwd: tmpProjPath(), + }).toString(); + expect(result).toContain('Hello World!'); + await killPorts(port); + }, 300_000); + + it('should be able to generate an express application', async () => { + const nodeapp = uniq('nodeapp'); + const nodelib = uniq('nodelib'); + const port = getRandomPort(); + process.env.PORT = `${port}`; + runCLI( + `generate @nx/express:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest` + ); + runCLI( + `generate @nx/node:lib packages/${nodelib} --linter=eslint --unitTestRunner=jest` + ); + + // No tests are generated by default, add a stub one. + updateFile( + `apps/${nodeapp}/src/app/test.spec.ts`, + ` + describe('test', () => { + it('should work', () => { + expect(true).toEqual(true); + }) + }) + ` + ); + + updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`); + updateFile(`apps/${nodeapp}/src/main.ts`, (content) => { + return `import { ${nodelib} } from '@proj/${nodelib}';\n${content}\nconsole.log(${nodelib}());`; + }); + // pnpm does not link packages unless they are deps + // npm, yarn, and bun will link them in the root node_modules regardless + if (pm === 'pnpm') { + updateJson(`apps/${nodeapp}/package.json`, (json) => { + json.dependencies = { + [`@proj/${nodelib}`]: 'workspace:', + }; + return json; + }); + runCommand(getPackageManagerCommand().install); + } + runCLI(`sync`); + + expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow(); + expect(() => runCLI(`test ${nodeapp}`)).not.toThrow(); + expect(() => runCLI(`build ${nodeapp}`)).not.toThrow(); + expect(() => runCLI(`lint ${nodelib}`)).not.toThrow(); + expect(() => runCLI(`test ${nodelib}`)).not.toThrow(); + expect(() => runCLI(`build ${nodelib}`)).not.toThrow(); + + const p = await runCommandUntil( + `serve ${nodeapp}`, + (output) => output.includes(`Listening at http://localhost:${port}`), + + { + env: { + NX_DAEMON: 'true', + }, + } + ); + + let result = await getData(port); + expect(result.message).toMatch(`Welcome to ${nodeapp}!`); + + result = await getData(port, '/assets/file.txt'); + expect(result).toMatch(`Test`); + + try { + await promisifiedTreeKill(p.pid, 'SIGKILL'); + expect(await killPorts(port)).toBeTruthy(); + } catch (err) { + expect(err).toBeFalsy(); + } + }, 300_000); +}); + +function getRandomPort() { + return Math.floor(1000 + Math.random() * 9000); +} + +function getData(port, path = '/api'): Promise { + return new Promise((resolve) => { + http.get(`http://localhost:${port}${path}`, (res) => { + expect(res.statusCode).toEqual(200); + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.once('end', () => { + try { + resolve(JSON.parse(data)); + } catch (e) { + resolve(data); + } + }); + }); + }); +} diff --git a/e2e/node/src/node-webpack.test.ts b/e2e/node/src/node-webpack.test.ts index f38b87f752f66..2d98ee18ea71d 100644 --- a/e2e/node/src/node-webpack.test.ts +++ b/e2e/node/src/node-webpack.test.ts @@ -30,7 +30,7 @@ describe('Node Applications + webpack', () => { // This fails with Crystal enabled because `--optimization` is not a correct flag to pass to `webpack`. runCLI( - `generate @nx/node:app apps/${app} --bundler=webpack --no-interactive`, + `generate @nx/node:app apps/${app} --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest`, { env: { NX_ADD_PLUGINS: 'false' }, } diff --git a/e2e/node/src/node.test.ts b/e2e/node/src/node.test.ts index e5955ac93ec9b..a9d52b3d44aeb 100644 --- a/e2e/node/src/node.test.ts +++ b/e2e/node/src/node.test.ts @@ -72,11 +72,11 @@ describe('Node Applications', () => { const port = getRandomPort(); process.env.PORT = `${port}`; runCLI( - `generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint` + `generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest` ); - const lintResults = runCLI(`lint ${nodeapp}`); - expect(lintResults).toContain('Successfully ran target lint'); + expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow(); + expect(() => runCLI(`test ${nodeapp}`)).not.toThrow(); updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`); await runCLIAsync(`build ${nodeapp}`); @@ -92,7 +92,9 @@ describe('Node Applications', () => { // TODO(crystal, @ndcunningham): This does not work because NxWebpackPlugin({}) outputFilename does not work. xit('should be able to generate the correct outputFileName in options', async () => { const nodeapp = uniq('nodeapp'); - runCLI(`generate @nx/node:app apps/${nodeapp} --linter=eslint`); + runCLI( + `generate @nx/node:app apps/${nodeapp} --linter=eslint --unitTestRunner=jest` + ); updateJson(join('apps', nodeapp, 'project.json'), (config) => { config.targets.build.options.outputFileName = 'index.js'; @@ -108,7 +110,7 @@ describe('Node Applications', () => { const port = getRandomPort(); process.env.PORT = `${port}`; runCLI( - `generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --bundler=webpack` + `generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --bundler=webpack --unitTestRunner=jest` ); const lintResults = runCLI(`lint ${nodeapp}`); @@ -190,7 +192,7 @@ module.exports = { const nodeapp = uniq('nodeapp'); runCLI( - `generate @nx/node:app apps/${nodeapp} --linter=eslint --bundler=webpack --framework=none` + `generate @nx/node:app apps/${nodeapp} --linter=eslint --bundler=webpack --framework=none --unitTestRunner=jest` ); updateFile('.env', `NX_FOOBAR="test foo bar"`); @@ -227,7 +229,9 @@ module.exports = { it("should exclude 'test' target from e2e project that uses jest", async () => { const appName = uniq('nodeapp'); - runCLI(`generate @nx/node:app ${appName} --no-interactive`); + runCLI( + `generate @nx/node:app ${appName} --no-interactive --unitTestRunner=jest --linter=eslint --e2eTestRunner=jest` + ); const nxJson = JSON.parse(readFile('nx.json')); expect(nxJson.plugins).toBeDefined(); @@ -246,7 +250,7 @@ module.exports = { process.env.PORT = `${port}`; runCLI( - `generate @nx/express:app apps/${nodeapp} --port=${port} --linter=eslint` + `generate @nx/express:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest` ); const lintResults = runCLI(`lint ${nodeapp}`); @@ -296,7 +300,9 @@ module.exports = { it('should be able to generate a nest application', async () => { const nestapp = uniq('nestapp'); const port = 3335; - runCLI(`generate @nx/nest:app apps/${nestapp} --linter=eslint`); + runCLI( + `generate @nx/nest:app apps/${nestapp} --linter=eslint --unitTestRunner=jest` + ); const lintResults = runCLI(`lint ${nestapp}`); expect(lintResults).toContain('Successfully ran target lint'); @@ -344,7 +350,7 @@ module.exports = { const nestapp = 'node-nest-docker-test'; runCLI( - `generate @nx/node:app ${nestapp} --bundler=webpack --framework=nest --docker` + `generate @nx/node:app ${nestapp} --bundler=webpack --framework=nest --docker --unitTestRunner=jest` ); checkFilesExist(`${nestapp}/Dockerfile`); @@ -359,7 +365,7 @@ module.exports = { const esmapp = uniq('esmapp'); runCLI( - `generate @nx/node:app ${esmapp} --linter=eslint --framework=none --bundler=webpack` + `generate @nx/node:app ${esmapp} --linter=eslint --framework=none --bundler=webpack --unitTestRunner=jest` ); updateJson(`apps/${esmapp}/tsconfig.app.json`, (config) => { config.module = 'esnext'; @@ -424,7 +430,9 @@ describe('Build Node apps', () => { xit('should generate a package.json with the `--generatePackageJson` flag', async () => { const packageManager = detectPackageManager(tmpProjPath()); const nestapp = uniq('nestapp'); - runCLI(`generate @nx/nest:app apps/${nestapp} --linter=eslint`); + runCLI( + `generate @nx/nest:app apps/${nestapp} --linter=eslint --unitTestRunner=jest` + ); await runCLIAsync(`build ${nestapp} --generatePackageJson`); @@ -485,7 +493,9 @@ describe('Build Node apps', () => { }); const nodeapp = uniq('nodeapp'); - runCLI(`generate @nx/node:app ${nodeapp} --bundler=webpack`); + runCLI( + `generate @nx/node:app ${nodeapp} --bundler=webpack --unitTestRunner=jest --linter=eslint` + ); const jslib = uniq('jslib'); runCLI(`generate @nx/js:lib ${jslib} --bundler=tsc`); @@ -529,7 +539,7 @@ ${jslib}(); process.env.PORT = `${port}`; runCLI( - `generate @nx/node:app apps/${appName} --port=${port} --no-interactive` + `generate @nx/node:app apps/${appName} --port=${port} --no-interactive --linter=eslint --unitTestRunner=jest` ); // deleteOutputPath should default to true @@ -569,7 +579,9 @@ ${jslib}(); const port = getRandomPort(); process.env.PORT = `${port}`; - runCLI(`generate @nx/node:app ${appName} --port=${port} --no-interactive`); + runCLI( + `generate @nx/node:app ${appName} --port=${port} --no-interactive --linter=eslint --unitTestRunner=jest` + ); // check files are generated without the layout directory ("apps/") and // using the project name as the directory when no directory is provided @@ -584,7 +596,9 @@ ${jslib}(); `Successfully ran target test for project ${appName}` ); - runCLI(`generate @nx/node:lib ${libName} --buildable --no-interactive`); + runCLI( + `generate @nx/node:lib ${libName} --buildable --no-interactive --linter=eslint --unitTestRunner=jest` + ); // check files are generated without the layout directory ("libs/") and // using the project name as the directory when no directory is provided @@ -605,7 +619,9 @@ ${jslib}(); // TODO(crystal, @ndcunningham): What is the alternative here? xit('should have plugin output if specified in `tsPlugins`', async () => { const nestapp = uniq('nestapp'); - runCLI(`generate @nx/nest:app ${nestapp} --linter=eslint`); + runCLI( + `generate @nx/nest:app ${nestapp} --linter=eslint --unitTestRunner=jest` + ); packageInstall('@nestjs/swagger', undefined, '^7.0.0'); @@ -659,7 +675,9 @@ ${jslib}(); describe('nest libraries', function () { it('should be able to generate a nest library', async () => { const nestlib = uniq('nestlib'); - runCLI(`generate @nx/nest:lib libs/${nestlib}`); + runCLI( + `generate @nx/nest:lib libs/${nestlib} --linter=eslint --unitTestRunner=jest` + ); const lintResults = runCLI(`lint ${nestlib}`); expect(lintResults).toContain('Successfully ran target lint'); @@ -673,7 +691,9 @@ ${jslib}(); it('should be able to generate a nest library w/ service', async () => { const nestlib = uniq('nestlib'); - runCLI(`generate @nx/nest:lib ${nestlib} --service`); + runCLI( + `generate @nx/nest:lib ${nestlib} --service --linter=eslint --unitTestRunner=jest` + ); const lintResults = runCLI(`lint ${nestlib}`); expect(lintResults).toContain('Successfully ran target lint'); @@ -687,7 +707,9 @@ ${jslib}(); it('should be able to generate a nest library w/ controller', async () => { const nestlib = uniq('nestlib'); - runCLI(`generate @nx/nest:lib ${nestlib} --controller`); + runCLI( + `generate @nx/nest:lib ${nestlib} --controller --linter=eslint --unitTestRunner=jest` + ); const lintResults = runCLI(`lint ${nestlib}`); expect(lintResults).toContain('Successfully ran target lint'); @@ -701,7 +723,9 @@ ${jslib}(); it('should be able to generate a nest library w/ controller and service', async () => { const nestlib = uniq('nestlib'); - runCLI(`generate @nx/nest:lib ${nestlib} --controller --service`); + runCLI( + `generate @nx/nest:lib ${nestlib} --controller --service --linter=eslint --unitTestRunner=jest` + ); const lintResults = runCLI(`lint ${nestlib}`); expect(lintResults).toContain('Successfully ran target lint'); @@ -714,7 +738,9 @@ ${jslib}(); it('should have plugin output if specified in `transformers`', async () => { const nestlib = uniq('nestlib'); - runCLI(`generate @nx/nest:lib libs/${nestlib} --buildable`); + runCLI( + `generate @nx/nest:lib libs/${nestlib} --buildable --linter=eslint --unitTestRunner=jest` + ); packageInstall('@nestjs/swagger', undefined, '^7.0.0'); @@ -758,9 +784,5 @@ exports.FooModel = FooModel; ` ); }, 300000); - - it('should run default jest tests', async () => { - await expectJestTestsToPass('@nx/node:lib'); - }, 100000); }); }); diff --git a/e2e/nuxt/src/nuxt.test.ts b/e2e/nuxt/src/nuxt.test.ts index 88ad5973d8473..c7e61b64e5fb1 100644 --- a/e2e/nuxt/src/nuxt.test.ts +++ b/e2e/nuxt/src/nuxt.test.ts @@ -16,7 +16,7 @@ describe('Nuxt Plugin', () => { packages: ['@nx/nuxt'], }); runCLI( - `generate @nx/nuxt:app ${app} --unitTestRunner=vitest --e2eTestRunner=cypress` + `generate @nx/nuxt:app ${app} --unitTestRunner=vitest --e2eTestRunner=cypress --linter=eslint` ); runCLI( `generate @nx/nuxt:component ${app}/src/components/one/one --name=one --unitTestRunner=vitest` diff --git a/e2e/nx/src/run.test.ts b/e2e/nx/src/run.test.ts index 6792fd43abeb0..e71ee43661c7a 100644 --- a/e2e/nx/src/run.test.ts +++ b/e2e/nx/src/run.test.ts @@ -589,7 +589,7 @@ describe('Nx Running Tests', () => { `generate @nx/js:lib ${libC} --bundler=tsc --defaults --tags=ui-b,shared --directory=libs/${libC}` ); runCLI( - `generate @nx/node:lib ${libD} --defaults --tags=api --directory=libs/${libD}` + `generate @nx/node:lib ${libD} --defaults --tags=api --directory=libs/${libD} --buildable=false` ); // libA depends on libC diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 7199e95e8623c..d0401f06d0f01 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -76,10 +76,12 @@ export function newProject({ name = uniq('proj'), packageManager = getSelectedPackageManager(), packages, + preset = 'apps', }: { name?: string; packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun'; readonly packages?: Array; + preset?: string; } = {}): string { const newProjectStart = performance.mark('new-project:start'); try { @@ -93,7 +95,7 @@ export function newProject({ 'create-nx-workspace:start' ); runCreateWorkspace(projScope, { - preset: 'apps', + preset, packageManager, }); const createNxWorkspaceEnd = performance.mark('create-nx-workspace:end'); diff --git a/e2e/vue/src/vue-ts-solution.test.ts b/e2e/vue/src/vue-ts-solution.test.ts new file mode 100644 index 0000000000000..50a932ad73e54 --- /dev/null +++ b/e2e/vue/src/vue-ts-solution.test.ts @@ -0,0 +1,75 @@ +import { + cleanupProject, + getSelectedPackageManager, + newProject, + runCLI, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; + +describe('Vue Plugin', () => { + let proj: string; + + const pm = getSelectedPackageManager(); + + beforeAll(() => { + proj = newProject({ + packages: ['@nx/vue'], + preset: 'ts', + }); + if (pm === 'pnpm') { + updateFile( + 'pnpm-workspace.yaml', + ` +packages: + - 'apps/**' + - 'packages/**' +` + ); + } else { + updateJson('package.json', (json) => { + json.workspaces = ['apps/**', 'packages/**']; + return json; + }); + } + }); + + afterAll(() => cleanupProject()); + + it('should serve application in dev mode', async () => { + const app = uniq('app'); + const lib = uniq('lib'); + runCLI( + `generate @nx/vue:app apps/${app} --unitTestRunner=vitest --e2eTestRunner=playwright --linter=eslint` + ); + runCLI( + `generate @nx/vue:lib packages/${lib} --bundler=vite --unitTestRunner=vitest --linter=eslint` + ); + + // app and lib generators don't have specs by default, add some stubs + updateFile( + `apps/${app}/src/foo.spec.ts`, + ` + test('it should run', () => { + expect(true).toBeTruthy(); + }); + ` + ); + updateFile( + `packages/${lib}/src/foo.spec.ts`, + ` + test('it should run', () => { + expect(true).toBeTruthy(); + }); + ` + ); + + expect(() => runCLI(`lint ${app}`)).not.toThrow(); + expect(() => runCLI(`test ${app}`)).not.toThrow(); + expect(() => runCLI(`build ${app}`)).not.toThrow(); + expect(() => runCLI(`lint ${lib}`)).not.toThrow(); + expect(() => runCLI(`test ${lib}`)).not.toThrow(); + expect(() => runCLI(`build ${lib}`)).not.toThrow(); + }, 300_000); +}); diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 1fe2eace4247e..4b63a40c697e5 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -32,6 +32,9 @@ import { printSocialInformation } from '../src/utils/social-information'; interface BaseArguments extends CreateWorkspaceOptions { preset: Preset; + linter?: 'none' | 'eslint'; + formatter?: 'none' | 'prettier'; + workspaces?: boolean; } interface NoneArguments extends BaseArguments { @@ -39,7 +42,6 @@ interface NoneArguments extends BaseArguments { workspaceType?: 'package-based' | 'integrated' | 'standalone'; js?: boolean; appName?: string | undefined; - formatter?: 'none' | 'prettier'; } interface ReactArguments extends BaseArguments { @@ -52,9 +54,6 @@ interface ReactArguments extends BaseArguments { nextAppDir: boolean; nextSrcDir: boolean; e2eTestRunner: 'none' | 'cypress' | 'playwright'; - linter?: 'none' | 'eslint'; - formatter?: 'none' | 'prettier'; - workspaces?: boolean; } interface AngularArguments extends BaseArguments { @@ -730,6 +729,10 @@ async function determineVueOptions( let style: undefined | string = undefined; let appName: string; let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined; + let linter: undefined | 'none' | 'eslint'; + let formatter: undefined | 'none' | 'prettier'; + + const workspaces = parsedArgs.workspaces ?? false; if (parsedArgs.preset && parsedArgs.preset !== Preset.Vue) { preset = parsedArgs.preset; @@ -741,7 +744,9 @@ async function determineVueOptions( } else { const framework = await determineVueFramework(parsedArgs); - const workspaceType = await determineStandaloneOrMonorepo(); + const workspaceType = workspaces + ? 'monorepo' + : await determineStandaloneOrMonorepo(); if (workspaceType === 'standalone') { appName = parsedArgs.appName ?? parsedArgs.name; } else { @@ -798,7 +803,23 @@ async function determineVueOptions( style = reply.style; } - return { preset, style, appName, e2eTestRunner }; + if (workspaces) { + linter = await determineLinterOptions(parsedArgs); + formatter = await determineFormatterOptions(parsedArgs); + } else { + linter = 'eslint'; + formatter = 'prettier'; + } + + return { + preset, + style, + appName, + e2eTestRunner, + linter, + formatter, + workspaces, + }; } async function determineAngularOptions( @@ -967,6 +988,10 @@ async function determineNodeOptions( let appName: string; let framework: 'express' | 'fastify' | 'koa' | 'nest' | 'none'; let docker: boolean; + let linter: undefined | 'none' | 'eslint'; + let formatter: undefined | 'none' | 'prettier'; + + const workspaces = parsedArgs.workspaces ?? false; if (parsedArgs.preset) { preset = parsedArgs.preset; @@ -989,7 +1014,9 @@ async function determineNodeOptions( } else { framework = await determineNodeFramework(parsedArgs); - const workspaceType = await determineStandaloneOrMonorepo(); + const workspaceType = workspaces + ? 'monorepo' + : await determineStandaloneOrMonorepo(); if (workspaceType === 'standalone') { preset = Preset.NodeStandalone; appName = parsedArgs.name; @@ -1024,11 +1051,22 @@ async function determineNodeOptions( docker = reply.docker === 'Yes'; } + if (workspaces) { + linter = await determineLinterOptions(parsedArgs); + formatter = await determineFormatterOptions(parsedArgs); + } else { + linter = 'eslint'; + formatter = 'prettier'; + } + return { preset, appName, framework, docker, + linter, + formatter, + workspaces, }; } diff --git a/packages/express/package.json b/packages/express/package.json index 44108d69c08d5..1e777406eddcf 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -32,7 +32,6 @@ }, "dependencies": { "@nx/devkit": "file:../devkit", - "@nx/js": "file:../js", "@nx/node": "file:../node", "tslib": "^2.3.0" }, diff --git a/packages/express/src/generators/application/application.spec.ts b/packages/express/src/generators/application/application.spec.ts index 954fc1ba1d0e3..7de92c58e455d 100644 --- a/packages/express/src/generators/application/application.spec.ts +++ b/packages/express/src/generators/application/application.spec.ts @@ -1,4 +1,4 @@ -import { readJson, Tree } from '@nx/devkit'; +import { readJson, Tree, updateJson, writeJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { applicationGenerator } from './application'; import { Schema } from './schema'; @@ -134,4 +134,178 @@ 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', + } as Schema); + + expect(readJson(appTree, 'tsconfig.json').references) + .toMatchInlineSnapshot(` + [ + { + "path": "./myapp-e2e", + }, + { + "path": "./myapp", + }, + ] + `); + expect(readJson(appTree, 'myapp/package.json')).toMatchInlineSnapshot(` + { + "name": "@proj/myapp", + "nx": { + "name": "myapp", + "projectType": "application", + "sourceRoot": "myapp/src", + "targets": { + "build": { + "configurations": { + "development": {}, + "production": {}, + }, + "defaultConfiguration": "production", + "executor": "@nx/webpack:webpack", + "options": { + "assets": [ + "myapp/src/assets", + ], + "compiler": "tsc", + "main": "myapp/src/main.ts", + "outputPath": "dist/myapp", + "target": "node", + "tsConfig": "myapp/tsconfig.app.json", + "webpackConfig": "myapp/webpack.config.js", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + "lint": { + "executor": "@nx/eslint:lint", + }, + "serve": { + "configurations": { + "development": { + "buildTarget": "myapp:build:development", + }, + "production": { + "buildTarget": "myapp:build:production", + }, + }, + "defaultConfiguration": "development", + "dependsOn": [ + "build", + ], + "executor": "@nx/js:node", + "options": { + "buildTarget": "myapp:build", + "runBuildTargetDependencies": false, + }, + }, + "test": { + "options": { + "passWithNoTests": true, + }, + }, + }, + }, + "private": true, + "version": "0.0.1", + } + `); + expect(readJson(appTree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "esModuleInterop": true, + }, + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(appTree, 'myapp/tsconfig.app.json')) + .toMatchInlineSnapshot(` + { + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "out-tsc/myapp", + "rootDir": "src", + "types": [ + "node", + "express", + ], + }, + "exclude": [ + "dist", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts", + ], + } + `); + expect(readJson(appTree, 'myapp/tsconfig.spec.json')) + .toMatchInlineSnapshot(` + { + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "./out-tsc/jest", + "types": [ + "jest", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.app.json", + }, + ], + } + `); + }); + }); }); diff --git a/packages/express/src/generators/application/application.ts b/packages/express/src/generators/application/application.ts index a4721b25bf1c1..594a4bfbb4b7e 100644 --- a/packages/express/src/generators/application/application.ts +++ b/packages/express/src/generators/application/application.ts @@ -11,7 +11,6 @@ import { determineProjectNameAndRootOptions, ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { applicationGenerator as nodeApplicationGenerator } from '@nx/node'; import { tslibVersion } from '@nx/node/src/utils/versions'; import { join } from 'path'; @@ -75,8 +74,6 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { } export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'express', 'application'); - const options = await normalizeOptions(tree, schema); const tasks: GeneratorCallback[] = []; diff --git a/packages/express/src/generators/application/schema.json b/packages/express/src/generators/application/schema.json index a8a24d18cab04..0397e89e75745 100644 --- a/packages/express/src/generators/application/schema.json +++ b/packages/express/src/generators/application/schema.json @@ -33,14 +33,17 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?" }, "tags": { "type": "string", diff --git a/packages/express/src/generators/init/init.ts b/packages/express/src/generators/init/init.ts index bf6d07deca2dc..3321e30dc2a0d 100644 --- a/packages/express/src/generators/init/init.ts +++ b/packages/express/src/generators/init/init.ts @@ -6,7 +6,6 @@ import { runTasksInSerial, Tree, } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { expressVersion, nxVersion } from '../../utils/versions'; import type { Schema } from './schema'; @@ -29,8 +28,6 @@ function updateDependencies(tree: Tree, schema: Schema) { } export async function initGenerator(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'express', 'init'); - let installTask: GeneratorCallback = () => {}; if (!schema.skipPackageJson) { installTask = updateDependencies(tree, schema); diff --git a/packages/nest/src/generators/application/application.spec.ts b/packages/nest/src/generators/application/application.spec.ts index 3be9fd03f1e18..de6999353a90c 100644 --- a/packages/nest/src/generators/application/application.spec.ts +++ b/packages/nest/src/generators/application/application.spec.ts @@ -1,4 +1,4 @@ -import type { Tree } from '@nx/devkit'; +import { readJson, updateJson, writeJson, type Tree } from '@nx/devkit'; import * as devkit from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { applicationGenerator } from './application'; @@ -66,6 +66,11 @@ describe('application generator', () => { "runBuildTargetDependencies": false, }, }, + "test": { + "options": { + "passWithNoTests": true, + }, + }, }, } `); @@ -162,4 +167,169 @@ describe('application generator', () => { expect(projectConfigurations.get(`${appDirectory}-e2e`)).toBeUndefined(); }); }); + + 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, { + directory: 'myapp', + unitTestRunner: 'jest', + addPlugin: true, + }); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./myapp-e2e", + }, + { + "path": "./myapp", + }, + ] + `); + expect(readJson(tree, 'myapp/package.json')).toMatchInlineSnapshot(` + { + "name": "@proj/myapp", + "nx": { + "name": "myapp", + "projectType": "application", + "sourceRoot": "myapp/src", + "targets": { + "build": { + "configurations": { + "development": { + "args": [ + "node-env=development", + ], + }, + }, + "executor": "nx:run-commands", + "options": { + "args": [ + "node-env=production", + ], + "command": "webpack-cli build", + }, + }, + "serve": { + "configurations": { + "development": { + "buildTarget": "myapp:build:development", + }, + "production": { + "buildTarget": "myapp:build:production", + }, + }, + "defaultConfiguration": "development", + "dependsOn": [ + "build", + ], + "executor": "@nx/js:node", + "options": { + "buildTarget": "myapp:build", + "runBuildTargetDependencies": false, + }, + }, + "test": { + "options": { + "passWithNoTests": true, + }, + }, + }, + }, + "private": true, + "version": "0.0.1", + } + `); + expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "esModuleInterop": true, + }, + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.app.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "emitDecoratorMetadata": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "out-tsc/myapp", + "rootDir": "src", + "target": "es2021", + "types": [ + "node", + ], + }, + "exclude": [ + "dist", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts", + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "./out-tsc/jest", + "types": [ + "jest", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.app.json", + }, + ], + } + `); + }); + }); }); diff --git a/packages/nest/src/generators/application/application.ts b/packages/nest/src/generators/application/application.ts index 015bb798a0688..c7bc9861ca643 100644 --- a/packages/nest/src/generators/application/application.ts +++ b/packages/nest/src/generators/application/application.ts @@ -1,6 +1,5 @@ import type { GeneratorCallback, Tree } from '@nx/devkit'; import { formatFiles, runTasksInSerial } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { applicationGenerator as nodeApplicationGenerator } from '@nx/node'; import { initGenerator } from '../init/init'; @@ -27,8 +26,6 @@ export async function applicationGeneratorInternal( tree: Tree, rawOptions: ApplicationGeneratorOptions ): Promise { - assertNotUsingTsSolutionSetup(tree, 'nest', 'application'); - const options = await normalizeOptions(tree, rawOptions); const tasks: GeneratorCallback[] = []; diff --git a/packages/nest/src/generators/application/schema.d.ts b/packages/nest/src/generators/application/schema.d.ts index 05d781e9d612f..560673120495b 100644 --- a/packages/nest/src/generators/application/schema.d.ts +++ b/packages/nest/src/generators/application/schema.d.ts @@ -15,6 +15,7 @@ export interface ApplicationGeneratorOptions { rootProject?: boolean; strict?: boolean; addPlugin?: boolean; + useTsSolution?: boolean; } interface NormalizedOptions extends ApplicationGeneratorOptions { diff --git a/packages/nest/src/generators/application/schema.json b/packages/nest/src/generators/application/schema.json index f74ea1d563a71..0eb3ee8764f60 100644 --- a/packages/nest/src/generators/application/schema.json +++ b/packages/nest/src/generators/application/schema.json @@ -37,13 +37,16 @@ "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], - "default": "eslint" + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "description": "Test runner to use for unit tests.", "type": "string", "enum": ["jest", "none"], - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?" }, "e2eTestRunner": { "type": "string", diff --git a/packages/nest/src/generators/init/init.ts b/packages/nest/src/generators/init/init.ts index 5224b3330a903..876319c42687b 100644 --- a/packages/nest/src/generators/init/init.ts +++ b/packages/nest/src/generators/init/init.ts @@ -1,6 +1,5 @@ import type { GeneratorCallback, Tree } from '@nx/devkit'; import { formatFiles } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { addDependencies } from './lib'; import type { InitGeneratorOptions } from './schema'; @@ -9,8 +8,6 @@ export async function initGenerator( tree: Tree, options: InitGeneratorOptions ): Promise { - assertNotUsingTsSolutionSetup(tree, 'nest', 'init'); - let installPackagesTask: GeneratorCallback = () => {}; if (!options.skipPackageJson) { installPackagesTask = addDependencies(tree, options); diff --git a/packages/nest/src/generators/library/library.spec.ts b/packages/nest/src/generators/library/library.spec.ts index e71093a8ae464..235b4cf2f1074 100644 --- a/packages/nest/src/generators/library/library.spec.ts +++ b/packages/nest/src/generators/library/library.spec.ts @@ -1,6 +1,6 @@ import type { Tree } from '@nx/devkit'; import * as devkit from '@nx/devkit'; -import { readJson, readProjectConfiguration } from '@nx/devkit'; +import { readJson, readProjectConfiguration, writeJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { libraryGenerator } from './library'; @@ -345,4 +345,114 @@ describe('lib', () => { ).toBeTruthy(); }); }); + + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + devkit.updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*', 'apps/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { + composite: true, + declaration: true, + }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + }); + + it('should add project references when using TS solution', async () => { + await libraryGenerator(tree, { + directory: 'mylib', + unitTestRunner: 'jest', + }); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./mylib", + }, + ] + `); + expect(readJson(tree, 'mylib/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'mylib/tsconfig.lib.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "baseUrl": ".", + "emitDeclarationOnly": false, + "forceConsistentCasingInFileNames": true, + "importHelpers": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "target": "es6", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "types": [ + "node", + ], + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts", + ], + "references": [], + } + `); + expect(readJson(tree, 'mylib/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "outDir": "./out-tsc/jest", + "types": [ + "jest", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + ], + } + `); + }); + }); }); diff --git a/packages/nest/src/generators/library/library.ts b/packages/nest/src/generators/library/library.ts index 9c48d74cb0687..4f9731d14a281 100644 --- a/packages/nest/src/generators/library/library.ts +++ b/packages/nest/src/generators/library/library.ts @@ -1,7 +1,6 @@ import type { GeneratorCallback, Tree } from '@nx/devkit'; import { formatFiles, runTasksInSerial } from '@nx/devkit'; import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { addExportsToBarrelFile, addProject, @@ -30,8 +29,6 @@ export async function libraryGeneratorInternal( tree: Tree, rawOptions: LibraryGeneratorOptions ): Promise { - assertNotUsingTsSolutionSetup(tree, 'nest', 'library'); - const options = await normalizeOptions(tree, rawOptions); await jsLibraryGenerator(tree, toJsLibraryGeneratorOptions(options)); const initTask = await initGenerator(tree, rawOptions); diff --git a/packages/nest/src/generators/library/schema.json b/packages/nest/src/generators/library/schema.json index 63e70db440af0..5ad79a7373f6f 100644 --- a/packages/nest/src/generators/library/schema.json +++ b/packages/nest/src/generators/library/schema.json @@ -31,13 +31,17 @@ "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], - "default": "eslint" + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "description": "Test runner to use for unit tests.", "type": "string", "enum": ["jest", "none"], - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?", + "x-priority": "important" }, "tags": { "description": "Add tags to the library (used for linting).", diff --git a/packages/node/src/generators/application/application.spec.ts b/packages/node/src/generators/application/application.spec.ts index d4e1a1af545d0..3c64143e1b473 100644 --- a/packages/node/src/generators/application/application.spec.ts +++ b/packages/node/src/generators/application/application.spec.ts @@ -6,6 +6,8 @@ import { readJson, readProjectConfiguration, Tree, + updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -59,6 +61,11 @@ describe('app', () => { "runBuildTargetDependencies": false, }, }, + "test": { + "options": { + "passWithNoTests": true, + }, + }, }, } `); @@ -266,6 +273,11 @@ describe('app', () => { "runBuildTargetDependencies": false, }, }, + "test": { + "options": { + "passWithNoTests": true, + }, + }, }, } `); @@ -548,4 +560,152 @@ describe('app', () => { } }); }); + + 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, { + directory: 'myapp', + bundler: 'webpack', + unitTestRunner: 'jest', + addPlugin: true, + }); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./myapp-e2e", + }, + { + "path": "./myapp", + }, + ] + `); + expect(readJson(tree, 'myapp/package.json')).toMatchInlineSnapshot(` + { + "name": "@proj/myapp", + "nx": { + "name": "myapp", + "projectType": "application", + "sourceRoot": "myapp/src", + "targets": { + "serve": { + "configurations": { + "development": { + "buildTarget": "myapp:build:development", + }, + "production": { + "buildTarget": "myapp:build:production", + }, + }, + "defaultConfiguration": "development", + "dependsOn": [ + "build", + ], + "executor": "@nx/js:node", + "options": { + "buildTarget": "myapp:build", + "runBuildTargetDependencies": false, + }, + }, + "test": { + "options": { + "passWithNoTests": true, + }, + }, + }, + }, + "private": true, + "version": "0.0.1", + } + `); + expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "esModuleInterop": true, + }, + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.app.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "out-tsc/myapp", + "rootDir": "src", + "types": [ + "node", + ], + }, + "exclude": [ + "dist", + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts", + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "./out-tsc/jest", + "types": [ + "jest", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.app.json", + }, + ], + } + `); + }); + }); }); diff --git a/packages/node/src/generators/application/application.ts b/packages/node/src/generators/application/application.ts index 58b808302c2e4..9709a8d7f62cb 100644 --- a/packages/node/src/generators/application/application.ts +++ b/packages/node/src/generators/application/application.ts @@ -1,3 +1,4 @@ +import { getImportPath } from '@nx/js/src/utils/get-import-path'; import { addDependenciesToPackageJson, addProjectConfiguration, @@ -19,6 +20,7 @@ import { updateJson, updateProjectConfiguration, updateTsConfigsToJs, + writeJson, } from '@nx/devkit'; import { determineProjectNameAndRootOptions, @@ -30,7 +32,6 @@ import { initGenerator as jsInitGenerator, tsConfigBaseOptions, } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { esbuildVersion } from '@nx/js/src/utils/versions'; import { Linter, lintProjectGenerator } from '@nx/eslint'; import { join } from 'path'; @@ -54,11 +55,16 @@ import { Schema } from './schema'; import { hasWebpackPlugin } from '../../utils/has-webpack-plugin'; import { addBuildTargetDefaults } from '@nx/devkit/src/generators/target-defaults-utils'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; +import { + isUsingTsSolutionSetup, + updateTsconfigFiles, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; export interface NormalizedSchema extends Schema { appProjectRoot: string; parsedTags: string[]; outputPath: string; + isUsingTsSolutionConfig: boolean; } function getWebpackBuildConfig( @@ -83,7 +89,7 @@ function getWebpackBuildConfig( options.appProjectRoot, 'webpack.config.js' ), - generatePackageJson: true, + generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true, }, configurations: { development: {}, @@ -114,7 +120,7 @@ function getEsBuildConfig( ), tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'), assets: [joinPathFragments(project.sourceRoot, 'assets')], - generatePackageJson: true, + generatePackageJson: options.isUsingTsSolutionConfig ? undefined : true, esbuildOptions: { sourcemap: true, // Generate CJS files as .js so imports can be './foo' rather than './foo.cjs'. @@ -198,16 +204,30 @@ function addProject(tree: Tree, options: NormalizedSchema) { } project.targets.serve = getServeConfig(options); - addProjectConfiguration( - tree, - options.name, - project, - options.standaloneConfig - ); + if (options.isUsingTsSolutionConfig) { + writeJson(tree, joinPathFragments(options.appProjectRoot, 'package.json'), { + name: getImportPath(tree, options.name), + version: '0.0.1', + private: true, + nx: { + name: options.name, + projectType: 'application', + sourceRoot: project.sourceRoot, + targets: project.targets, + tags: project.tags?.length ? project.tags : undefined, + }, + }); + } else { + addProjectConfiguration( + tree, + options.name, + project, + options.standaloneConfig + ); + } } function addAppFiles(tree: Tree, options: NormalizedSchema) { - const sourceRoot = joinPathFragments(options.appProjectRoot, 'src'); generateFiles( tree, join(__dirname, './files/common'), @@ -410,10 +430,17 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { } export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'node', 'application'); + const tasks: GeneratorCallback[] = []; + + const jsInitTask = await jsInitGenerator(tree, { + ...schema, + tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', + skipFormat: true, + addTsPlugin: schema.useTsSolution, + }); + tasks.push(jsInitTask); const options = await normalizeOptions(tree, schema); - const tasks: GeneratorCallback[] = []; if (options.framework === 'nest') { // nx-ignore-next-line @@ -442,12 +469,6 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { ); } - const jsInitTask = await jsInitGenerator(tree, { - ...schema, - tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', - skipFormat: true, - }); - tasks.push(jsInitTask); const initTask = await initGenerator(tree, { ...schema, skipFormat: true, @@ -501,6 +522,13 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { skipFormat: true, }); tasks.push(jestTask); + // There are no tests by default, so set `--passWithNoTests` to avoid test failure on new project. + const projectConfig = readProjectConfiguration(tree, options.name); + projectConfig.targets ??= {}; + projectConfig.targets.test = { + options: { passWithNoTests: true }, + }; + updateProjectConfiguration(tree, options.name, projectConfig); } else { // No need for default spec file if unit testing is not setup. tree.delete( @@ -544,6 +572,21 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) { await formatFiles(tree); } + if (options.isUsingTsSolutionConfig) { + updateTsconfigFiles( + tree, + options.appProjectRoot, + 'tsconfig.app.json', + { + module: 'nodenext', + moduleResolution: 'nodenext', + }, + options.linter === 'eslint' + ? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'] + : undefined + ); + } + tasks.push(() => { logShowProjectCommand(options.name); }); @@ -594,6 +637,7 @@ async function normalizeOptions( 'dist', options.rootProject ? options.name : appProjectRoot ), + isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), }; } diff --git a/packages/node/src/generators/application/schema.d.ts b/packages/node/src/generators/application/schema.d.ts index e5a15e8a94968..914fe3e591c99 100644 --- a/packages/node/src/generators/application/schema.d.ts +++ b/packages/node/src/generators/application/schema.d.ts @@ -8,6 +8,7 @@ export interface Schema { unitTestRunner?: 'jest' | 'none'; e2eTestRunner?: 'jest' | 'none'; linter?: Linter | LinterType; + formatter?: 'none' | 'prettier'; tags?: string; frontendProject?: string; swcJest?: boolean; @@ -23,6 +24,7 @@ export interface Schema { docker?: boolean; isNest?: boolean; addPlugin?: boolean; + useTsSolution?: boolean; } export type NodeJsFrameWorks = 'express' | 'koa' | 'fastify' | 'nest' | 'none'; diff --git a/packages/node/src/generators/application/schema.json b/packages/node/src/generators/application/schema.json index ed4a8ae569346..114ca3d0f0982 100644 --- a/packages/node/src/generators/application/schema.json +++ b/packages/node/src/generators/application/schema.json @@ -36,14 +36,26 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "default": "none", + "x-priority": "important", + "x-prompt": "Which unit test runner would you like to use?" + }, + "e2eTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for end-to-end tests", + "default": "none", + "x-priority": "important", + "x-prompt": "Which end-to-end test runner would you like to use?" }, "tags": { "type": "string", @@ -109,12 +121,6 @@ "hidden": true, "x-priority": "internal" }, - "e2eTestRunner": { - "type": "string", - "enum": ["jest", "none"], - "description": "Test runner to use for end to end (e2e) tests", - "default": "jest" - }, "docker": { "type": "boolean", "description": "Add a docker build target" diff --git a/packages/node/src/generators/e2e-project/e2e-project.spec.ts b/packages/node/src/generators/e2e-project/e2e-project.spec.ts index 2ac6fc5983a16..9a9ac35950494 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.spec.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.spec.ts @@ -1,6 +1,6 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; -import { Tree } from '@nx/devkit'; +import { readJson, Tree, updateJson, writeJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { applicationGenerator } from '../application/application'; import { e2eProjectGenerator } from './e2e-project'; @@ -74,4 +74,92 @@ describe('e2eProjectGenerator', () => { " `); }); + + 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, { + directory: 'api', + framework: 'none', + e2eTestRunner: 'none', + addPlugin: true, + }); + await e2eProjectGenerator(tree, { + projectType: 'server', + project: 'api', + addPlugin: true, + }); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./api", + }, + { + "path": "./api-e2e", + }, + ] + `); + expect(tree.read('api-e2e/jest.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "export default { + displayName: 'api-e2e', + preset: '../jest.preset.js', + globalSetup: '/src/support/global-setup.ts', + globalTeardown: '/src/support/global-teardown.ts', + setupFiles: ['/src/support/test-setup.ts'], + testEnvironment: 'node', + transform: { + '^.+\\\\.[tj]s$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.json', + }, + ], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../coverage/api-e2e', + }; + " + `); + expect(readJson(tree, 'api-e2e/tsconfig.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "esModuleInterop": true, + "noImplicitAny": false, + "noUnusedLocals": false, + "outDir": "out-tsc/api-e2e", + }, + "extends": "../tsconfig.base.json", + "include": [ + "jest.config.ts", + "src/**/*.ts", + ], + "references": [ + { + "path": "../api", + }, + ], + } + `); + }); + }); }); diff --git a/packages/node/src/generators/e2e-project/e2e-project.ts b/packages/node/src/generators/e2e-project/e2e-project.ts index 44cf8afbc1f20..a69fc31527ade 100644 --- a/packages/node/src/generators/e2e-project/e2e-project.ts +++ b/packages/node/src/generators/e2e-project/e2e-project.ts @@ -12,6 +12,7 @@ import { runTasksInSerial, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { Linter, lintProjectGenerator } from '@nx/eslint'; @@ -29,6 +30,9 @@ import { } from '@nx/eslint/src/generators/utils/eslint-file'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { findRootJestPreset } from '@nx/jest/src/utils/config/config-file'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { relative } from 'node:path/posix'; export async function e2eProjectGenerator(host: Tree, options: Schema) { return await e2eProjectGeneratorInternal(host, { @@ -44,24 +48,49 @@ export async function e2eProjectGeneratorInternal( const tasks: GeneratorCallback[] = []; const options = await normalizeOptions(host, _options); const appProject = readProjectConfiguration(host, options.project); + const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); // TODO(@ndcunningham): This is broken.. the outputs are wrong.. and this isn't using the jest generator - addProjectConfiguration(host, options.e2eProjectName, { - root: options.e2eProjectRoot, - implicitDependencies: [options.project], - projectType: 'application', - targets: { - e2e: { - executor: '@nx/jest:jest', - outputs: ['{workspaceRoot}/coverage/{e2eProjectRoot}'], - options: { - jestConfig: `${options.e2eProjectRoot}/jest.config.ts`, - passWithNoTests: true, + if (isUsingTsSolutionConfig) { + writeJson(host, joinPathFragments(options.e2eProjectRoot, 'package.json'), { + name: getImportPath(host, options.e2eProjectName), + version: '0.0.1', + private: true, + nx: { + name: options.e2eProjectName, + projectType: 'application', + implicitDependencies: [options.project], + targets: { + e2e: { + executor: '@nx/jest:jest', + outputs: ['{workspaceRoot}/coverage/{e2eProjectRoot}'], + options: { + jestConfig: `${options.e2eProjectRoot}/jest.config.ts`, + passWithNoTests: true, + }, + dependsOn: [`${options.project}:build`], + }, }, - dependsOn: [`${options.project}:build`], }, - }, - }); + }); + } else { + addProjectConfiguration(host, options.e2eProjectName, { + root: options.e2eProjectRoot, + implicitDependencies: [options.project], + projectType: 'application', + targets: { + e2e: { + executor: '@nx/jest:jest', + outputs: ['{workspaceRoot}/coverage/{e2eProjectRoot}'], + options: { + jestConfig: `${options.e2eProjectRoot}/jest.config.ts`, + passWithNoTests: true, + }, + dependsOn: [`${options.project}:build`], + }, + }, + }); + } // TODO(@nicholas): Find a better way to get build target // We remove the 'test' target from the e2e project because it is not needed @@ -91,6 +120,9 @@ export async function e2eProjectGeneratorInternal( } const jestPreset = findRootJestPreset(host) ?? 'jest.preset.js'; + const tsConfigFile = isUsingTsSolutionConfig + ? 'tsconfig.json' + : 'tsconfig.spec.json'; if (options.projectType === 'server') { generateFiles( host, @@ -99,6 +131,7 @@ export async function e2eProjectGeneratorInternal( { ...options, ...names(options.rootProject ? 'server' : options.project), + tsConfigFile, offsetFromRoot: offsetFromRoot(options.e2eProjectRoot), jestPreset, tmpl: '', @@ -113,6 +146,7 @@ export async function e2eProjectGeneratorInternal( { ...options, ...names(options.rootProject ? 'server' : options.project), + tsConfigFile, offsetFromRoot: offsetFromRoot(options.e2eProjectRoot), tmpl: '', } @@ -128,6 +162,7 @@ export async function e2eProjectGeneratorInternal( ...options, ...names(options.rootProject ? 'cli' : options.project), mainFile, + tsConfigFile, offsetFromRoot: offsetFromRoot(options.e2eProjectRoot), jestPreset, tmpl: '', @@ -135,6 +170,34 @@ export async function e2eProjectGeneratorInternal( ); } + if (isUsingTsSolutionConfig) { + generateFiles( + host, + path.join(__dirname, 'files/ts-solution'), + options.e2eProjectRoot, + { + ...options, + relativeProjectReferencePath: relative( + options.e2eProjectRoot, + appProject.root + ), + offsetFromRoot: offsetFromRoot(options.e2eProjectRoot), + tmpl: '', + } + ); + } else { + generateFiles( + host, + path.join(__dirname, 'files/non-ts-solution'), + options.e2eProjectRoot, + { + ...options, + offsetFromRoot: offsetFromRoot(options.e2eProjectRoot), + tmpl: '', + } + ); + } + // axios is more than likely used in the application code, so install it as a regular dependency. const installTask = addDependenciesToPackageJson( host, @@ -171,6 +234,17 @@ export async function e2eProjectGeneratorInternal( await formatFiles(host); } + if (isUsingTsSolutionConfig) { + updateJson(host, 'tsconfig.json', (json) => { + json.references ??= []; + const e2eRef = `./${options.e2eProjectRoot}`; + if (!json.references.find((ref) => ref.path === e2eRef)) { + json.references.push({ path: e2eRef }); + } + return json; + }); + } + tasks.push(() => { logShowProjectCommand(options.e2eProjectName); }); diff --git a/packages/node/src/generators/e2e-project/files/cli/jest.config.ts__tmpl__ b/packages/node/src/generators/e2e-project/files/cli/jest.config.ts__tmpl__ index 3b0e4fbff3681..fe96a5081ddf3 100644 --- a/packages/node/src/generators/e2e-project/files/cli/jest.config.ts__tmpl__ +++ b/packages/node/src/generators/e2e-project/files/cli/jest.config.ts__tmpl__ @@ -5,7 +5,7 @@ export default { testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: '/<%= tsConfigFile %>', }], }, moduleFileExtensions: ['ts', 'js', 'html'], diff --git a/packages/node/src/generators/e2e-project/files/cli/tsconfig.spec.json__tmpl__ b/packages/node/src/generators/e2e-project/files/cli/tsconfig.spec.json__tmpl__ deleted file mode 100644 index 2c05fb02dcbab..0000000000000 --- a/packages/node/src/generators/e2e-project/files/cli/tsconfig.spec.json__tmpl__ +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "<%= offsetFromRoot %>/dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.ts" - ] -} diff --git a/packages/node/src/generators/e2e-project/files/cli/tsconfig.json__tmpl__ b/packages/node/src/generators/e2e-project/files/non-ts-solution/tsconfig.json__tmpl__ similarity index 100% rename from packages/node/src/generators/e2e-project/files/cli/tsconfig.json__tmpl__ rename to packages/node/src/generators/e2e-project/files/non-ts-solution/tsconfig.json__tmpl__ diff --git a/packages/node/src/generators/e2e-project/files/server/common/tsconfig.spec.json__tmpl__ b/packages/node/src/generators/e2e-project/files/non-ts-solution/tsconfig.spec.json__tmpl__ similarity index 100% rename from packages/node/src/generators/e2e-project/files/server/common/tsconfig.spec.json__tmpl__ rename to packages/node/src/generators/e2e-project/files/non-ts-solution/tsconfig.spec.json__tmpl__ diff --git a/packages/node/src/generators/e2e-project/files/server/common/jest.config.ts__tmpl__ b/packages/node/src/generators/e2e-project/files/server/common/jest.config.ts__tmpl__ index 73b4a8b6ca7ad..329afa69d90f2 100644 --- a/packages/node/src/generators/e2e-project/files/server/common/jest.config.ts__tmpl__ +++ b/packages/node/src/generators/e2e-project/files/server/common/jest.config.ts__tmpl__ @@ -7,7 +7,7 @@ export default { testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { - tsconfig: '/tsconfig.spec.json', + tsconfig: '/<%= tsConfigFile %>', }], }, moduleFileExtensions: ['ts', 'js', 'html'], diff --git a/packages/node/src/generators/e2e-project/files/server/common/tsconfig.json__tmpl__ b/packages/node/src/generators/e2e-project/files/server/common/tsconfig.json__tmpl__ deleted file mode 100644 index a038446e2badf..0000000000000 --- a/packages/node/src/generators/e2e-project/files/server/common/tsconfig.json__tmpl__ +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "<%= offsetFromRoot %><% if (rootProject) { %>tsconfig.json<% } else { %>tsconfig.base.json<% } %>", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/packages/node/src/generators/e2e-project/files/ts-solution/tsconfig.json__tmpl__ b/packages/node/src/generators/e2e-project/files/ts-solution/tsconfig.json__tmpl__ new file mode 100644 index 0000000000000..069363a8be3cd --- /dev/null +++ b/packages/node/src/generators/e2e-project/files/ts-solution/tsconfig.json__tmpl__ @@ -0,0 +1,16 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "outDir": "out-tsc/<%= e2eProjectName %>", + "esModuleInterop": true, + "noUnusedLocals": false, + "noImplicitAny": false + }, + "include": [ + "jest.config.ts", + "src/**/*.ts" + ], + "references": [ + { "path": "<%= relativeProjectReferencePath %>" } + ] +} diff --git a/packages/node/src/generators/init/init.ts b/packages/node/src/generators/init/init.ts index 95b34a31344bc..a7d1dce3122e0 100644 --- a/packages/node/src/generators/init/init.ts +++ b/packages/node/src/generators/init/init.ts @@ -6,7 +6,6 @@ import { runTasksInSerial, Tree, } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { nxVersion } from '../../utils/versions'; import { Schema } from './schema'; @@ -27,8 +26,6 @@ function updateDependencies(tree: Tree, options: Schema) { } export async function initGenerator(tree: Tree, options: Schema) { - assertNotUsingTsSolutionSetup(tree, 'node', 'init'); - let installTask: GeneratorCallback = () => {}; if (!options.skipPackageJson) { installTask = updateDependencies(tree, options); diff --git a/packages/node/src/generators/library/files/lib/package.json__tmpl__ b/packages/node/src/generators/library/files/lib/package.json__tmpl__ deleted file mode 100644 index e3a3ad83c46eb..0000000000000 --- a/packages/node/src/generators/library/files/lib/package.json__tmpl__ +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "<%= importPath %>", - "version": "0.0.1" -} diff --git a/packages/node/src/generators/library/files/lib/tsconfig.lib.json b/packages/node/src/generators/library/files/non-ts-solution/tsconfig.lib.json similarity index 100% rename from packages/node/src/generators/library/files/lib/tsconfig.lib.json rename to packages/node/src/generators/library/files/non-ts-solution/tsconfig.lib.json diff --git a/packages/node/src/generators/library/files/ts-solution/tsconfig.lib.json b/packages/node/src/generators/library/files/ts-solution/tsconfig.lib.json new file mode 100644 index 0000000000000..c9a30870bad24 --- /dev/null +++ b/packages/node/src/generators/library/files/ts-solution/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/packages/node/src/generators/library/library.spec.ts b/packages/node/src/generators/library/library.spec.ts index ae8b2c5b43a5e..b4e5a051cf347 100644 --- a/packages/node/src/generators/library/library.spec.ts +++ b/packages/node/src/generators/library/library.spec.ts @@ -5,6 +5,8 @@ import { readJson, readProjectConfiguration, Tree, + updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; @@ -474,7 +476,7 @@ describe('lib', () => { 'src/**/*.js', ]); expect(readJson(tree, 'my-lib/tsconfig.lib.json').exclude).toEqual([ - 'jest.config.ts', + 'jest.config.js', 'src/**/*.spec.ts', 'src/**/*.test.ts', 'src/**/*.spec.js', @@ -517,4 +519,163 @@ describe('lib', () => { expect(tree.exists('my-dir/my-lib/src/lib/my-lib.spec.js')).toBeTruthy(); }); }); + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*', 'apps/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { + composite: true, + declaration: true, + }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + }); + + it('should add project references when using TS solution', async () => { + await libraryGenerator(tree, { + directory: 'mylib', + unitTestRunner: 'jest', + addPlugin: true, + } as Schema); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./mylib", + }, + ] + `); + expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(` + { + "dependencies": {}, + "main": "./src/index.ts", + "name": "@proj/mylib", + "nx": { + "name": "mylib", + "projectType": "library", + "sourceRoot": "mylib/src", + }, + "private": true, + "types": "./src/index.ts", + "version": "0.0.1", + } + `); + expect(readJson(tree, 'mylib/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'mylib/tsconfig.lib.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "baseUrl": ".", + "emitDeclarationOnly": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "dist", + "rootDir": "src", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "types": [ + "node", + ], + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.ts", + ], + "references": [], + } + `); + expect(readJson(tree, 'mylib/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "outDir": "./out-tsc/jest", + "types": [ + "jest", + "node", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + ], + } + `); + }); + + it('should set correct options for swc', async () => { + await libraryGenerator(tree, { + directory: 'mylib', + buildable: true, + compiler: 'swc', + unitTestRunner: 'jest', + addPlugin: true, + } as Schema); + + expect(readJson(tree, 'mylib/package.json')).toMatchInlineSnapshot(` + { + "dependencies": { + "tslib": "^2.3.0", + }, + "main": "./dist/index.js", + "name": "@proj/mylib", + "nx": { + "name": "mylib", + "projectType": "library", + "sourceRoot": "mylib/src", + "targets": { + "build": { + "executor": "@nx/js:swc", + "options": { + "main": "mylib/src/index.ts", + "outputPath": "mylib/dist", + "packageJson": "mylib/package.json", + "stripLeadingPaths": true, + "tsConfig": "mylib/tsconfig.lib.json", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + }, + }, + "private": true, + "type": "commonjs", + "typings": "./dist/index.d.ts", + "version": "0.0.1", + } + `); + }); + }); }); diff --git a/packages/node/src/generators/library/library.ts b/packages/node/src/generators/library/library.ts index d5eef8892372c..68a3dcdbc7c60 100644 --- a/packages/node/src/generators/library/library.ts +++ b/packages/node/src/generators/library/library.ts @@ -3,6 +3,7 @@ import { formatFiles, generateFiles, GeneratorCallback, + installPackagesTask, joinPathFragments, names, offsetFromRoot, @@ -13,6 +14,7 @@ import { Tree, updateProjectConfiguration, updateTsConfigsToJs, + writeJson, } from '@nx/devkit'; import { determineProjectNameAndRootOptions, @@ -21,12 +23,13 @@ import { import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; import { addSwcConfig } from '@nx/js/src/utils/swc/add-swc-config'; import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { join } from 'path'; import { tslibVersion, typesNodeVersion } from '../../utils/versions'; import { initGenerator } from '../init/init'; import { Schema } from './schema'; 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 interface NormalizedSchema extends Schema { fileName: string; @@ -34,6 +37,7 @@ export interface NormalizedSchema extends Schema { projectRoot: string; parsedTags: string[]; compiler: 'swc' | 'tsc'; + isUsingTsSolutionConfig: boolean; } export async function libraryGenerator(tree: Tree, schema: Schema) { @@ -44,15 +48,8 @@ export async function libraryGenerator(tree: Tree, schema: Schema) { } export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'node', 'library'); - const options = await normalizeOptions(tree, schema); - const tasks: GeneratorCallback[] = [ - await initGenerator(tree, { - ...options, - skipFormat: true, - }), - ]; + const tasks: GeneratorCallback[] = []; if (options.publishable === true && !schema.importPath) { throw new Error( @@ -60,16 +57,40 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { ); } - const libraryInstall = await jsLibraryGenerator(tree, { - ...options, - bundler: schema.buildable || schema.publishable ? 'tsc' : 'none', - includeBabelRc: schema.babelJest, - importPath: options.importPath, - testEnvironment: 'node', - skipFormat: true, - setParserOptionsProject: options.setParserOptionsProject, - }); - tasks.push(libraryInstall); + // Create `package.json` first because @nx/js:lib generator will update it. + if ( + options.isUsingTsSolutionConfig || + options.publishable || + options.buildable + ) { + writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), { + name: getImportPath(tree, options.name), + version: '0.0.1', + private: true, + files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, + }); + } + + tasks.push( + await jsLibraryGenerator(tree, { + ...schema, + bundler: schema.buildable || schema.publishable ? 'tsc' : 'none', + includeBabelRc: schema.babelJest, + importPath: schema.importPath, + testEnvironment: 'node', + skipFormat: true, + setParserOptionsProject: schema.setParserOptionsProject, + useProjectJson: !options.isUsingTsSolutionConfig, + }) + ); + + tasks.push( + await initGenerator(tree, { + ...options, + skipFormat: true, + }) + ); + createFiles(tree, options); if (options.js) { @@ -79,6 +100,11 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { tasks.push(ensureDependencies(tree)); + // Always run install to link packages. + if (options.isUsingTsSolutionConfig) { + tasks.push(() => installPackagesTask(tree, true)); + } + if (!schema.skipFormat) { await formatFiles(tree); } @@ -129,6 +155,7 @@ async function normalizeOptions( projectRoot, parsedTags, importPath, + isUsingTsSolutionConfig: isUsingTsSolutionSetup(tree), }; } @@ -149,9 +176,6 @@ function createFiles(tree: Tree, options: NormalizedSchema) { join(options.projectRoot, `./src/lib/${options.fileName}.spec.ts`) ); } - if (!options.publishable && !options.buildable) { - tree.delete(join(options.projectRoot, 'package.json')); - } if (options.js) { toJS(tree); } @@ -167,20 +191,29 @@ function updateProject(tree: Tree, options: NormalizedSchema) { project.targets = project.targets || {}; addBuildTargetDefaults(tree, `@nx/js:${options.compiler}`); - project.targets.build = { - executor: `@nx/js:${options.compiler}`, - outputs: ['{options.outputPath}'], - options: { - outputPath: joinPathFragments( - 'dist', - rootProject ? options.projectName : options.projectRoot - ), - tsConfig: `${options.projectRoot}/tsconfig.lib.json`, - packageJson: `${options.projectRoot}/package.json`, - main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'), - assets: [`${options.projectRoot}/*.md`], - }, - }; + + // For TS solution, we want tsc build to be inferred by `@nx/js/typescript` plugin. + if (!options.isUsingTsSolutionConfig || options.compiler === 'swc') { + project.targets.build = { + executor: `@nx/js:${options.compiler}`, + outputs: ['{options.outputPath}'], + options: { + outputPath: options.isUsingTsSolutionConfig + ? joinPathFragments(options.projectRoot, 'dist') + : joinPathFragments( + 'dist', + rootProject ? options.projectName : options.projectRoot + ), + tsConfig: `${options.projectRoot}/tsconfig.lib.json`, + packageJson: `${options.projectRoot}/package.json`, + main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'), + assets: options.isUsingTsSolutionConfig + ? undefined + : [`${options.projectRoot}/*.md`], + stripLeadingPaths: options.isUsingTsSolutionConfig ? true : undefined, + }, + }; + } if (options.compiler === 'swc') { addSwcDependencies(tree); diff --git a/packages/node/src/generators/library/schema.json b/packages/node/src/generators/library/schema.json index 51124a0a3f973..0649384de0bb7 100644 --- a/packages/node/src/generators/library/schema.json +++ b/packages/node/src/generators/library/schema.json @@ -36,14 +36,18 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "default": "none", + "x-prompt": "Which unit test runner would you like to use?", + "x-priority": "important" }, "tags": { "type": "string", @@ -69,7 +73,7 @@ }, "buildable": { "type": "boolean", - "default": false, + "default": true, "description": "Generate a buildable library.", "x-priority": "important" }, diff --git a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap index 67c3d76774431..f080ce37ee798 100644 --- a/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/nuxt/src/generators/application/__snapshots__/application.spec.ts.snap @@ -108,7 +108,6 @@ exports[`app generated files content - as-provided - my-app general application exports[`app generated files content - as-provided - my-app general application should configure tsconfig and project.json correctly 2`] = ` "{ - "compilerOptions": {}, "files": [], "include": [".nuxt/nuxt.d.ts"], "references": [ @@ -190,7 +189,6 @@ exports[`app generated files content - as-provided - my-app general application exports[`app generated files content - as-provided - my-app general application should configure vitest correctly 3`] = ` "{ - "compilerOptions": {}, "files": [], "include": [".nuxt/nuxt.d.ts"], "references": [ @@ -469,7 +467,6 @@ exports[`app generated files content - as-provided - myApp general application s exports[`app generated files content - as-provided - myApp general application should configure tsconfig and project.json correctly 2`] = ` "{ - "compilerOptions": {}, "files": [], "include": [".nuxt/nuxt.d.ts"], "references": [ @@ -551,7 +548,6 @@ exports[`app generated files content - as-provided - myApp general application s exports[`app generated files content - as-provided - myApp general application should configure vitest correctly 3`] = ` "{ - "compilerOptions": {}, "files": [], "include": [".nuxt/nuxt.d.ts"], "references": [ diff --git a/packages/nuxt/src/generators/application/application.spec.ts b/packages/nuxt/src/generators/application/application.spec.ts index f26160511b18b..d5c222c75a2d1 100644 --- a/packages/nuxt/src/generators/application/application.spec.ts +++ b/packages/nuxt/src/generators/application/application.spec.ts @@ -1,7 +1,13 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; -import { Tree, readJson, readProjectConfiguration } from '@nx/devkit'; +import { + Tree, + readJson, + readProjectConfiguration, + updateJson, + writeJson, +} from '@nx/devkit'; import { applicationGenerator } from './application'; describe('app', () => { @@ -194,4 +200,173 @@ describe('app', () => { }); } ); + + 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, { + directory: 'myapp', + e2eTestRunner: 'playwright', + unitTestRunner: 'vitest', + linter: 'eslint', + }); + + expect(tree.read('myapp/vite.config.ts', 'utf-8')).toMatchInlineSnapshot( + `null` + ); + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./myapp-e2e", + }, + { + "path": "./myapp", + }, + ] + `); + expect(readJson(tree, 'myapp/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.app.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "composite": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "out-tsc/myapp", + "resolveJsonModule": true, + "rootDir": "src", + }, + "exclude": [ + "dist", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + ".nuxt/nuxt.d.ts", + "src/**/*", + ], + } + `); + expect(readJson(tree, 'myapp/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "composite": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "./out-tsc/vitest", + "resolveJsonModule": true, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + ".nuxt/nuxt.d.ts", + "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(tree, 'myapp-e2e/tsconfig.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "allowJs": true, + "outDir": "dist", + "sourceMap": false, + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", + }, + "exclude": [ + "dist", + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "**/*.ts", + "**/*.js", + "playwright.config.ts", + "src/**/*.spec.ts", + "src/**/*.spec.js", + "src/**/*.test.ts", + "src/**/*.test.js", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "../myapp", + }, + ], + } + `); + }); + }); }); diff --git a/packages/nuxt/src/generators/application/application.ts b/packages/nuxt/src/generators/application/application.ts index 0ce521f9139e9..b1d9dec26a1e4 100644 --- a/packages/nuxt/src/generators/application/application.ts +++ b/packages/nuxt/src/generators/application/application.ts @@ -9,6 +9,7 @@ import { runTasksInSerial, toJS, Tree, + writeJson, } from '@nx/devkit'; import { Schema } from './schema'; import nuxtInitGenerator from '../init/init'; @@ -18,7 +19,6 @@ import { getRelativePathToRootTsConfig, initGenerator as jsInitGenerator, } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { updateGitIgnore } from '../../utils/update-gitignore'; import { Linter } from '@nx/eslint'; import { addE2e } from './lib/add-e2e'; @@ -32,12 +32,20 @@ import { getNxCloudAppOnBoardingUrl, createNxCloudOnboardingURLForWelcomeApp, } from 'nx/src/nx-cloud/utilities/onboarding'; +import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; export async function applicationGenerator(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'nuxt', 'application'); - const tasks: GeneratorCallback[] = []; + const jsInitTask = await jsInitGenerator(tree, { + ...schema, + tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', + skipFormat: true, + addTsPlugin: schema.useTsSolution, + }); + tasks.push(jsInitTask); + const options = await normalizeOptions(tree, schema); const projectOffsetFromRoot = offsetFromRoot(options.appProjectRoot); @@ -51,20 +59,29 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { onBoardingStatus === 'unclaimed' && (await getNxCloudAppOnBoardingUrl(options.nxCloudToken)); - const jsInitTask = await jsInitGenerator(tree, { - ...schema, - tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', - skipFormat: true, - }); - tasks.push(jsInitTask); tasks.push(ensureDependencies(tree, options)); - addProjectConfiguration(tree, options.projectName, { - root: options.appProjectRoot, - projectType: 'application', - sourceRoot: `${options.appProjectRoot}/src`, - targets: {}, - }); + if (options.isUsingTsSolutionConfig) { + writeJson(tree, joinPathFragments(options.appProjectRoot, 'package.json'), { + name: getImportPath(tree, 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, + }, + }); + } else { + addProjectConfiguration(tree, options.projectName, { + root: options.appProjectRoot, + projectType: 'application', + sourceRoot: `${options.appProjectRoot}/src`, + tags: options.parsedTags?.length ? options.parsedTags : undefined, + targets: {}, + }); + } generateFiles( tree, @@ -111,6 +128,7 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { projectRoot: options.appProjectRoot, rootProject: options.rootProject, unitTestRunner: options.unitTestRunner, + isUsingTsSolutionConfig: options.isUsingTsSolutionConfig, }, getRelativePathToRootTsConfig(tree, options.appProjectRoot) ); @@ -168,6 +186,24 @@ export async function applicationGenerator(tree: Tree, schema: Schema) { } }); + if (options.isUsingTsSolutionConfig) { + updateTsconfigFiles( + tree, + options.appProjectRoot, + 'tsconfig.app.json', + { + jsx: 'preserve', + jsxImportSource: 'vue', + module: 'esnext', + moduleResolution: 'bundler', + resolveJsonModule: true, + }, + options.linter === 'eslint' + ? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'] + : undefined + ); + } + tasks.push(() => { logShowProjectCommand(options.projectName); }); diff --git a/packages/nuxt/src/generators/application/lib/normalize-options.ts b/packages/nuxt/src/generators/application/lib/normalize-options.ts index d35182f941624..4fdee758de7a6 100644 --- a/packages/nuxt/src/generators/application/lib/normalize-options.ts +++ b/packages/nuxt/src/generators/application/lib/normalize-options.ts @@ -4,6 +4,7 @@ import { ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { NormalizedSchema, Schema } from '../schema'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export async function normalizeOptions( host: Tree, @@ -35,6 +36,7 @@ export async function normalizeOptions( e2eProjectRoot, parsedTags, style: options.style ?? 'none', + isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), } as NormalizedSchema; normalized.unitTestRunner ??= 'vitest'; diff --git a/packages/nuxt/src/generators/application/schema.d.ts b/packages/nuxt/src/generators/application/schema.d.ts index a758930a817ad..bad65c175ad89 100644 --- a/packages/nuxt/src/generators/application/schema.d.ts +++ b/packages/nuxt/src/generators/application/schema.d.ts @@ -4,6 +4,7 @@ export interface Schema { directory: string; name?: string; linter?: Linter | LinterType; + formatter?: 'none' | 'prettier'; skipFormat?: boolean; unitTestRunner?: 'vitest' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'none'; @@ -14,6 +15,7 @@ export interface Schema { setParserOptionsProject?: boolean; style?: 'css' | 'scss' | 'less' | 'none'; nxCloudToken?: string; + useTsSolution?: boolean; } export interface NormalizedSchema extends Schema { @@ -22,4 +24,5 @@ export interface NormalizedSchema extends Schema { e2eProjectName: string; e2eProjectRoot: string; parsedTags: string[]; + isUsingTsSolutionConfig: boolean; } diff --git a/packages/nuxt/src/generators/application/schema.json b/packages/nuxt/src/generators/application/schema.json index effd221b7efea..75f4fd461a4e8 100644 --- a/packages/nuxt/src/generators/application/schema.json +++ b/packages/nuxt/src/generators/application/schema.json @@ -22,24 +22,27 @@ "pattern": "^[a-zA-Z][^:]*$", "x-priority": "important" }, - "linter": { - "description": "The tool to use for running lint checks.", - "type": "string", - "enum": ["eslint"], - "default": "eslint" - }, "skipFormat": { "description": "Skip formatting files.", "type": "boolean", "default": false, "x-priority": "internal" }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" + }, "unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "description": "Test runner to use for unit tests.", "x-prompt": "Which unit test runner would you like to use?", - "default": "none" + "default": "none", + "x-priority": "important" }, "e2eTestRunner": { "type": "string", diff --git a/packages/nuxt/src/generators/init/init.ts b/packages/nuxt/src/generators/init/init.ts index 5eef7f78ea477..1bc971e19b904 100644 --- a/packages/nuxt/src/generators/init/init.ts +++ b/packages/nuxt/src/generators/init/init.ts @@ -1,14 +1,11 @@ import { createProjectGraphAsync, GeneratorCallback, Tree } from '@nx/devkit'; import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { createNodes } from '../../plugins/plugin'; import { InitSchema } from './schema'; import { updateDependencies } from './lib/utils'; export async function nuxtInitGenerator(host: Tree, schema: InitSchema) { - assertNotUsingTsSolutionSetup(host, 'nuxt', 'init'); - await addPluginV1( host, await createProjectGraphAsync(), diff --git a/packages/nuxt/src/utils/create-ts-config.ts b/packages/nuxt/src/utils/create-ts-config.ts index 46e9ed6ce268e..63683b7f1707a 100644 --- a/packages/nuxt/src/utils/create-ts-config.ts +++ b/packages/nuxt/src/utils/create-ts-config.ts @@ -7,14 +7,14 @@ export function createTsConfig( projectRoot: string; rootProject?: boolean; unitTestRunner?: string; + isUsingTsSolutionConfig: boolean; }, relativePathToRootTsConfig: string ) { createAppTsConfig(host, options); const json = { - compilerOptions: {}, files: [], - include: ['.nuxt/nuxt.d.ts'], + include: options.isUsingTsSolutionConfig ? undefined : ['.nuxt/nuxt.d.ts'], references: [ { path: './tsconfig.app.json', diff --git a/packages/playwright/src/generators/configuration/configuration.ts b/packages/playwright/src/generators/configuration/configuration.ts index e245e8c4d2296..5e9d6030a5f20 100644 --- a/packages/playwright/src/generators/configuration/configuration.ts +++ b/packages/playwright/src/generators/configuration/configuration.ts @@ -97,7 +97,12 @@ export async function configurationGeneratorInternal( if (isTsSolutionSetup) { // skip eslint from typechecking since it extends from root file that is outside rootDir if (options.linter === 'eslint') { - tsconfig.exclude = ['dist', 'eslint.config.js']; + tsconfig.exclude = [ + 'dist', + 'eslint.config.js', + 'eslint.config.mjs', + 'eslint.config.cjs', + ]; } tsconfig.compilerOptions.outDir = 'dist'; diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 44826502ba28d..3da3698851e8e 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -1419,6 +1419,8 @@ describe('app', () => { "exclude": [ "dist", "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", ], "extends": "../tsconfig.base.json", "include": [ diff --git a/packages/react/src/generators/library/library.spec.ts b/packages/react/src/generators/library/library.spec.ts index 940024b1cab18..13a46b13c809d 100644 --- a/packages/react/src/generators/library/library.spec.ts +++ b/packages/react/src/generators/library/library.spec.ts @@ -1001,7 +1001,7 @@ module.exports = withNx( include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { - reportsDirectory: '../coverage/mylib', + reportsDirectory: './test-output/vitest/coverage', provider: 'v8', } }, diff --git a/packages/remix/src/generators/application/application.impl.spec.ts b/packages/remix/src/generators/application/application.impl.spec.ts index cc510d65b853a..9737ca910b0e4 100644 --- a/packages/remix/src/generators/application/application.impl.spec.ts +++ b/packages/remix/src/generators/application/application.impl.spec.ts @@ -507,6 +507,8 @@ describe('Remix Application', () => { "exclude": [ "dist", "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", ], "extends": "../tsconfig.base.json", "include": [ diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index 1c7a3d88c0472..1087bbfc17a11 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -460,10 +460,11 @@ export function createOrEditViteConfig( ); } - const reportsDirectory = - projectRoot === '.' - ? `./coverage/${options.project}` - : `${offsetFromRoot(projectRoot)}coverage/${projectRoot}`; + const reportsDirectory = isUsingTsPlugin + ? './test-output/vitest/coverage' + : projectRoot === '.' + ? `./coverage/${options.project}` + : `${offsetFromRoot(projectRoot)}coverage/${projectRoot}`; const testOption = options.includeVitest ? ` test: { diff --git a/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap index 84f0aba45dcf7..2944d649a680a 100644 --- a/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/vue/src/generators/application/__snapshots__/application.spec.ts.snap @@ -361,6 +361,7 @@ exports[`application generator should set up project correctly with given option "test/src/app/NxWelcome.vue", "test/src/main.ts", "test/src/styles.css", + "test/src/vue-shims.d.ts", "test/tsconfig.app.json", "test/tsconfig.json", "test/tsconfig.spec.json", diff --git a/packages/vue/src/generators/application/application.spec.ts b/packages/vue/src/generators/application/application.spec.ts index 244728a729667..7aa6caa4ff498 100644 --- a/packages/vue/src/generators/application/application.spec.ts +++ b/packages/vue/src/generators/application/application.spec.ts @@ -6,6 +6,9 @@ import { readProjectConfiguration, readNxJson, updateNxJson, + updateJson, + writeJson, + readJson, } from '@nx/devkit'; import * as devkitExports from 'nx/src/devkit-exports'; @@ -96,6 +99,188 @@ describe('application generator', () => { expect(tree.exists('test/src/style.none')).toBeFalsy(); expect(tree.read('test/src/main.ts', 'utf-8')).not.toContain('styles.none'); }); + + 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, { + ...options, + style: 'none', + linter: 'eslint', + }); + + expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchInlineSnapshot(` + "/// + import { defineConfig } from 'vite'; + import vue from '@vitejs/plugin-vue'; + + export default defineConfig({ + root: __dirname, + cacheDir: '../node_modules/.vite/test', + server: { + port: 4200, + host: 'localhost', + }, + preview: { + port: 4300, + host: 'localhost', + }, + plugins: [vue()], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + build: { + outDir: './dist', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8', + }, + }, + }); + " + `); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./test-e2e", + }, + { + "path": "./test", + }, + ] + `); + expect(readJson(tree, 'test/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'test/tsconfig.app.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "out-tsc/test", + "resolveJsonModule": true, + "rootDir": "src", + "types": [ + "vite/client", + ], + }, + "exclude": [ + "dist", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.vue", + "src/**/*.test.vue", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.vue", + ], + } + `); + expect(readJson(tree, 'test/tsconfig.spec.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "./out-tsc/vitest", + "resolveJsonModule": true, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.app.json", + }, + ], + } + `); + }); + }); }); function listFiles(tree: Tree): string[] { diff --git a/packages/vue/src/generators/application/application.ts b/packages/vue/src/generators/application/application.ts index 3b85e58abfd9c..b76078b87115c 100644 --- a/packages/vue/src/generators/application/application.ts +++ b/packages/vue/src/generators/application/application.ts @@ -2,14 +2,15 @@ import { addProjectConfiguration, formatFiles, GeneratorCallback, + joinPathFragments, readNxJson, runTasksInSerial, toJS, Tree, + writeJson, } from '@nx/devkit'; import { Linter } from '@nx/eslint'; import { initGenerator as jsInitGenerator } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { Schema } from './schema'; import { normalizeOptions } from './lib/normalize-options'; import { vueInitGenerator } from '../init/init'; @@ -20,6 +21,8 @@ import { addVite } from './lib/add-vite'; import { extractTsConfigBase } from '../../utils/create-ts-config'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; +import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function applicationGenerator(tree: Tree, options: Schema) { return applicationGeneratorInternal(tree, { addPlugin: false, ...options }); @@ -29,7 +32,18 @@ export async function applicationGeneratorInternal( tree: Tree, _options: Schema ): Promise { - assertNotUsingTsSolutionSetup(tree, 'vue', 'application'); + const tasks: GeneratorCallback[] = []; + tasks.push( + await jsInitGenerator(tree, { + ..._options, + tsConfigName: _options.rootProject + ? 'tsconfig.json' + : 'tsconfig.base.json', + skipFormat: true, + addTsPlugin: _options.useTsSolution, + formatter: _options.formatter, + }) + ); const options = await normalizeOptions(tree, _options); const nxJson = readNxJson(tree); @@ -38,24 +52,28 @@ export async function applicationGeneratorInternal( process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false; - const tasks: GeneratorCallback[] = []; - - addProjectConfiguration(tree, options.projectName, { - root: options.appProjectRoot, - projectType: 'application', - sourceRoot: `${options.appProjectRoot}/src`, - targets: {}, - }); + if (options.isUsingTsSolutionConfig) { + writeJson(tree, joinPathFragments(options.appProjectRoot, 'package.json'), { + name: getImportPath(tree, 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, + }, + }); + } else { + addProjectConfiguration(tree, options.projectName, { + root: options.appProjectRoot, + projectType: 'application', + sourceRoot: `${options.appProjectRoot}/src`, + tags: options.parsedTags?.length ? options.parsedTags : undefined, + targets: {}, + }); + } - tasks.push( - await jsInitGenerator(tree, { - ...options, - tsConfigName: options.rootProject - ? 'tsconfig.json' - : 'tsconfig.base.json', - skipFormat: true, - }) - ); tasks.push( await vueInitGenerator(tree, { ...options, @@ -97,6 +115,24 @@ export async function applicationGeneratorInternal( if (!options.skipFormat) await formatFiles(tree); + if (options.isUsingTsSolutionConfig) { + updateTsconfigFiles( + tree, + options.appProjectRoot, + 'tsconfig.app.json', + { + jsx: 'preserve', + jsxImportSource: 'vue', + module: 'esnext', + moduleResolution: 'bundler', + resolveJsonModule: true, + }, + options.linter === 'eslint' + ? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'] + : undefined + ); + } + tasks.push(() => { logShowProjectCommand(options.projectName); }); diff --git a/packages/vue/src/generators/application/files/common/src/vue-shims.d.ts.template b/packages/vue/src/generators/application/files/common/src/vue-shims.d.ts.template new file mode 100644 index 0000000000000..798e8fcfac4af --- /dev/null +++ b/packages/vue/src/generators/application/files/common/src/vue-shims.d.ts.template @@ -0,0 +1,5 @@ +declare module '*.vue' { + import { defineComponent } from 'vue'; + const component: ReturnType; + export default component; +} diff --git a/packages/vue/src/generators/application/lib/normalize-options.ts b/packages/vue/src/generators/application/lib/normalize-options.ts index bfefcd687d59f..5a8e8752b519d 100644 --- a/packages/vue/src/generators/application/lib/normalize-options.ts +++ b/packages/vue/src/generators/application/lib/normalize-options.ts @@ -4,6 +4,7 @@ import { ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { NormalizedSchema, Schema } from '../schema'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export async function normalizeOptions( host: Tree, @@ -39,6 +40,7 @@ export async function normalizeOptions( normalized.routing = normalized.routing ?? false; normalized.unitTestRunner ??= 'vitest'; normalized.e2eTestRunner = normalized.e2eTestRunner ?? 'playwright'; + normalized.isUsingTsSolutionConfig = isUsingTsSolutionSetup(host); return normalized; } diff --git a/packages/vue/src/generators/application/schema.d.ts b/packages/vue/src/generators/application/schema.d.ts index 46d4eb6e28715..b98873f749af3 100644 --- a/packages/vue/src/generators/application/schema.d.ts +++ b/packages/vue/src/generators/application/schema.d.ts @@ -10,6 +10,7 @@ export interface Schema { inSourceTests?: boolean; e2eTestRunner: 'cypress' | 'playwright' | 'none'; linter: Linter | LinterType; + formatter?: 'none' | 'prettier'; routing?: boolean; js?: boolean; strict?: boolean; @@ -18,6 +19,7 @@ export interface Schema { rootProject?: boolean; addPlugin?: boolean; nxCloudToken?: string; + useTsSolution?: boolean; } export interface NormalizedSchema extends Schema { @@ -27,4 +29,5 @@ export interface NormalizedSchema extends Schema { e2eProjectRoot: string; parsedTags: string[]; devServerPort?: number; + isUsingTsSolutionConfig: boolean; } diff --git a/packages/vue/src/generators/application/schema.json b/packages/vue/src/generators/application/schema.json index a063888fd1011..a18386d23db6b 100644 --- a/packages/vue/src/generators/application/schema.json +++ b/packages/vue/src/generators/application/schema.json @@ -60,12 +60,6 @@ ] } }, - "linter": { - "description": "The tool to use for running lint checks.", - "type": "string", - "enum": ["eslint", "none"], - "default": "eslint" - }, "routing": { "type": "boolean", "description": "Generate application with routes.", @@ -78,12 +72,21 @@ "default": false, "x-priority": "internal" }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "none"], + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" + }, "unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "description": "Test runner to use for unit tests.", "x-prompt": "Which unit test runner would you like to use?", - "default": "vitest" + "default": "none", + "x-priority": "important" }, "inSourceTests": { "type": "boolean", diff --git a/packages/vue/src/generators/init/init.ts b/packages/vue/src/generators/init/init.ts index d3bcdb0ed90cd..dc258ca2f940d 100755 --- a/packages/vue/src/generators/init/init.ts +++ b/packages/vue/src/generators/init/init.ts @@ -6,7 +6,6 @@ import { runTasksInSerial, Tree, } from '@nx/devkit'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { nxVersion, vueVersion } from '../../utils/versions'; import { InitSchema } from './schema'; @@ -28,8 +27,6 @@ function updateDependencies(host: Tree, schema: InitSchema) { } export async function vueInitGenerator(host: Tree, schema: InitSchema) { - assertNotUsingTsSolutionSetup(host, 'vue', 'init'); - let installTask: GeneratorCallback = () => {}; if (!schema.skipPackageJson) { installTask = updateDependencies(host, schema); diff --git a/packages/vue/src/generators/library/files/package.json__tmpl__ b/packages/vue/src/generators/library/files/package.json__tmpl__ deleted file mode 100644 index 507420ee30834..0000000000000 --- a/packages/vue/src/generators/library/files/package.json__tmpl__ +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "<%= name %>", - "version": "0.0.1", - "main": "./index.js", - "types": "./index.d.ts", - "exports": { - ".": { - "import": "./index.mjs", - "require": "./index.js" - } - } -} diff --git a/packages/vue/src/generators/library/lib/create-library-files.ts b/packages/vue/src/generators/library/lib/create-library-files.ts index 9e2c6ccb3b260..323a3d80f551b 100644 --- a/packages/vue/src/generators/library/lib/create-library-files.ts +++ b/packages/vue/src/generators/library/lib/create-library-files.ts @@ -31,8 +31,23 @@ export function createLibraryFiles(host: Tree, options: NormalizedSchema) { substitutions ); - if (!options.publishable && options.bundler === 'none') { - host.delete(`${options.projectRoot}/package.json`); + if ( + !options.isUsingTsSolutionConfig && + (options.publishable || options.bundler !== 'none') + ) { + writeJson(host, joinPathFragments(options.projectRoot, 'package.json'), { + name: options.name, + version: '0.0.1', + main: './index.js', + types: './index.d.ts', + exports: { + '.': { + import: './index.mjs', + require: './index.js', + types: './index.d.ts', + }, + }, + }); } if (options.unitTestRunner !== 'vitest') { diff --git a/packages/vue/src/generators/library/lib/normalize-options.ts b/packages/vue/src/generators/library/lib/normalize-options.ts index 2a91e17a1c734..eabe320f65238 100644 --- a/packages/vue/src/generators/library/lib/normalize-options.ts +++ b/packages/vue/src/generators/library/lib/normalize-options.ts @@ -10,6 +10,7 @@ import { ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { NormalizedSchema, Schema } from '../schema'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export async function normalizeOptions( host: Tree, @@ -60,6 +61,7 @@ export async function normalizeOptions( projectRoot, parsedTags, importPath, + isUsingTsSolutionConfig: isUsingTsSolutionSetup(host), } as NormalizedSchema; // Libraries with a bundler or is publishable must also be buildable. diff --git a/packages/vue/src/generators/library/library.spec.ts b/packages/vue/src/generators/library/library.spec.ts index 94092f41301e2..79ebba4e39a17 100644 --- a/packages/vue/src/generators/library/library.spec.ts +++ b/packages/vue/src/generators/library/library.spec.ts @@ -5,6 +5,7 @@ import { readProjectConfiguration, Tree, updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '@nx/eslint'; @@ -438,4 +439,212 @@ module.exports = [ expect(eslintConfig.overrides[0].files).toContain('*.vue'); }); }); + describe('TS solution setup', () => { + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*', 'apps/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { + composite: true, + declaration: true, + }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + }); + + it('should add project references when using TS solution', async () => { + await libraryGenerator(tree, { + ...defaultSchema, + setParserOptionsProject: true, + linter: 'eslint', + }); + + expect(tree.read('my-lib/vite.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import vue from '@vitejs/plugin-vue'; + import { defineConfig } from 'vite'; + + export default defineConfig({ + root: __dirname, + cacheDir: '../node_modules/.vite/my-lib', + plugins: [vue()], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8', + }, + }, + }); + " + `); + + expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` + [ + { + "path": "./my-lib", + }, + ] + `); + expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + expect(readJson(tree, 'my-lib/tsconfig.lib.json')).toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "out-tsc/my-lib", + "resolveJsonModule": true, + "rootDir": "src", + "types": [ + "vite/client", + ], + }, + "exclude": [ + "dist", + "src/**/__tests__/*", + "src/**/*.spec.vue", + "src/**/*.test.vue", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.mjs", + ], + "extends": "../tsconfig.base.json", + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + ], + } + `); + expect(readJson(tree, 'my-lib/tsconfig.spec.json')) + .toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "./out-tsc/vitest", + "resolveJsonModule": true, + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest", + ], + }, + "extends": "../tsconfig.base.json", + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts", + ], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + ], + } + `); + }); + + it('should exclude non-buildable libraries from TS plugin registration', async () => { + updateJson(tree, 'nx.json', (json) => { + json.plugins = ['@nx/js/typescript']; + return json; + }); + await libraryGenerator(tree, { + ...defaultSchema, + addPlugin: true, + setParserOptionsProject: true, + linter: 'eslint', + bundler: 'none', + }); + + const nxJson = readJson(tree, 'nx.json'); + expect(nxJson.plugins).toMatchInlineSnapshot(` + [ + { + "exclude": [ + "my-lib/*", + ], + "plugin": "@nx/js/typescript", + }, + { + "options": { + "targetName": "lint", + }, + "plugin": "@nx/eslint/plugin", + }, + { + "options": { + "buildTargetName": "build", + "previewTargetName": "preview", + "serveStaticTargetName": "serve-static", + "serveTargetName": "serve", + "testTargetName": "test", + "typecheckTargetName": "typecheck", + }, + "plugin": "@nx/vite/plugin", + }, + ] + `); + }); + }); }); diff --git a/packages/vue/src/generators/library/library.ts b/packages/vue/src/generators/library/library.ts index 25a721b9da1e8..6b286ae765240 100644 --- a/packages/vue/src/generators/library/library.ts +++ b/packages/vue/src/generators/library/library.ts @@ -2,14 +2,17 @@ import { addProjectConfiguration, formatFiles, GeneratorCallback, + installPackagesTask, joinPathFragments, + readNxJson, runTasksInSerial, toJS, Tree, updateJson, + updateNxJson, + writeJson, } from '@nx/devkit'; import { addTsConfigPath, initGenerator as jsInitGenerator } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { vueInitGenerator } from '../init/init'; import { Schema } from './schema'; import { normalizeOptions } from './lib/normalize-options'; @@ -22,16 +25,19 @@ import { ensureDependencies } from '../../utils/ensure-dependencies'; import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command'; import { getRelativeCwd } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; import { relative } from 'path'; +import { getImportPath } from '@nx/js/src/utils/get-import-path'; +import { updateTsconfigFiles } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { ensureProjectIsExcludedFromPluginRegistrations } from '@nx/js/src/utils/typescript/plugin'; export function libraryGenerator(tree: Tree, schema: Schema) { return libraryGeneratorInternal(tree, { addPlugin: false, ...schema }); } export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'vue', 'library'); - const tasks: GeneratorCallback[] = []; + tasks.push(await jsInitGenerator(tree, { ...schema, skipFormat: true })); + const options = await normalizeOptions(tree, schema); if (options.publishable === true && !schema.importPath) { throw new Error( @@ -39,15 +45,43 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { ); } - addProjectConfiguration(tree, options.name, { - root: options.projectRoot, - sourceRoot: joinPathFragments(options.projectRoot, 'src'), - projectType: 'library', - tags: options.parsedTags, - targets: {}, - }); + if (options.isUsingTsSolutionConfig) { + const moduleFile = + options.bundler === 'none' + ? options.js + ? './src/index.js' + : './src/index.ts' + : './dist/index.mjs'; + const typesFile = + options.bundler === 'none' + ? options.js + ? './src/index.js' + : './src/index.ts' + : './dist/index.d.ts'; + writeJson(tree, joinPathFragments(options.projectRoot, 'package.json'), { + name: getImportPath(tree, options.name), + version: '0.0.1', + private: true, + module: moduleFile, + types: typesFile, + files: options.publishable ? ['dist', '!**/*.tsbuildinfo'] : undefined, + nx: { + name: options.name, + projectType: 'application', + sourceRoot: `${options.projectRoot}/src`, + tags: options.parsedTags?.length ? options.parsedTags : undefined, + }, + }); + } else { + addProjectConfiguration(tree, options.name, { + root: options.projectRoot, + sourceRoot: joinPathFragments(options.projectRoot, 'src'), + projectType: 'library', + tags: options.parsedTags, + targets: {}, + }); + } - tasks.push(await jsInitGenerator(tree, { ...schema, skipFormat: true })); tasks.push( await vueInitGenerator(tree, { ...options, @@ -86,14 +120,23 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { }); } - if (options.publishable || options.bundler !== 'none') { + if ( + !options.isUsingTsSolutionConfig && + (options.publishable || options.bundler !== 'none') + ) { updateJson(tree, `${options.projectRoot}/package.json`, (json) => { json.name = options.importPath; return json; }); } - if (!options.skipTsConfig) { + if (options.bundler === 'none') { + const nxJson = readNxJson(tree); + ensureProjectIsExcludedFromPluginRegistrations(nxJson, options.projectRoot); + updateNxJson(tree, nxJson); + } + + if (!options.skipTsConfig && !options.isUsingTsSolutionConfig) { addTsConfigPath(tree, options.importPath, [ joinPathFragments( options.projectRoot, @@ -107,6 +150,29 @@ export async function libraryGeneratorInternal(tree: Tree, schema: Schema) { if (!options.skipFormat) await formatFiles(tree); + if (options.isUsingTsSolutionConfig) { + updateTsconfigFiles( + tree, + options.projectRoot, + 'tsconfig.lib.json', + { + jsx: 'preserve', + jsxImportSource: 'vue', + module: 'esnext', + moduleResolution: 'bundler', + resolveJsonModule: true, + }, + options.linter === 'eslint' + ? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'] + : undefined + ); + } + + // Always run install to link packages. + if (options.isUsingTsSolutionConfig) { + tasks.push(() => installPackagesTask(tree, true)); + } + tasks.push(() => { logShowProjectCommand(options.name); }); diff --git a/packages/vue/src/generators/library/schema.d.ts b/packages/vue/src/generators/library/schema.d.ts index 198e8cd5840ba..ed8dc8249f096 100644 --- a/packages/vue/src/generators/library/schema.d.ts +++ b/packages/vue/src/generators/library/schema.d.ts @@ -35,4 +35,5 @@ export interface NormalizedSchema extends Schema { appMain?: string; appSourceRoot?: string; unitTestRunner?: 'vitest' | 'none'; + isUsingTsSolutionConfig: boolean; } diff --git a/packages/vue/src/generators/library/schema.json b/packages/vue/src/generators/library/schema.json index 7ca310f6b938d..dda7e5bbb558c 100644 --- a/packages/vue/src/generators/library/schema.json +++ b/packages/vue/src/generators/library/schema.json @@ -36,13 +36,17 @@ "description": "The tool to use for running lint checks.", "type": "string", "enum": ["eslint", "none"], - "default": "eslint" + "default": "none", + "x-prompt": "Which linter would you like to use?", + "x-priority": "important" }, "unitTestRunner": { "type": "string", "enum": ["vitest", "none"], "description": "Test runner to use for unit tests.", - "x-prompt": "What unit test runner should be used?" + "x-prompt": "What unit test runner should be used?", + "default": "none", + "x-priority": "important" }, "inSourceTests": { "type": "boolean", diff --git a/packages/vue/src/utils/create-ts-config.ts b/packages/vue/src/utils/create-ts-config.ts index 421303d8991b2..db671d8b7cf4e 100644 --- a/packages/vue/src/utils/create-ts-config.ts +++ b/packages/vue/src/utils/create-ts-config.ts @@ -1,7 +1,80 @@ import { Tree, updateJson, writeJson } from '@nx/devkit'; import * as shared from '@nx/js/src/utils/typescript/create-ts-config'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; export function createTsConfig( + host: Tree, + projectRoot: string, + type: 'app' | 'lib', + options: { + strict?: boolean; + style?: string; + bundler?: string; + rootProject?: boolean; + unitTestRunner?: string; + }, + relativePathToRootTsConfig: string +) { + if (isUsingTsSolutionSetup(host)) { + createTsConfigForTsSolution( + host, + projectRoot, + type, + options, + relativePathToRootTsConfig + ); + } else { + createTsConfigForNonTsSolution( + host, + projectRoot, + type, + options, + relativePathToRootTsConfig + ); + } +} + +export function createTsConfigForTsSolution( + host: Tree, + projectRoot: string, + type: 'app' | 'lib', + options: { + strict?: boolean; + style?: string; + rootProject?: boolean; + unitTestRunner?: string; + }, + relativePathToRootTsConfig: string +) { + const json = { + extends: relativePathToRootTsConfig, + files: [], + include: [], + references: [ + { + path: type === 'app' ? './tsconfig.app.json' : './tsconfig.lib.json', + }, + ], + } as any; + + writeJson(host, `${projectRoot}/tsconfig.json`, json); + + const tsconfigProjectPath = `${projectRoot}/tsconfig.${type}.json`; + if (host.exists(tsconfigProjectPath)) { + updateJson(host, tsconfigProjectPath, (json) => { + json.compilerOptions ??= {}; + + const types = new Set(json.compilerOptions.types ?? []); + types.add('vite/client'); + + json.compilerOptions.types = Array.from(types); + + return json; + }); + } +} + +export function createTsConfigForNonTsSolution( host: Tree, projectRoot: string, type: 'app' | 'lib', diff --git a/packages/workspace/src/generators/new/generate-workspace-files.ts b/packages/workspace/src/generators/new/generate-workspace-files.ts index da8bb75720680..fdca213300a2b 100644 --- a/packages/workspace/src/generators/new/generate-workspace-files.ts +++ b/packages/workspace/src/generators/new/generate-workspace-files.ts @@ -421,7 +421,11 @@ function setUpWorkspacesInPackageJson(tree: Tree, options: NormalizedSchema) { options.preset === Preset.NextJs || options.preset === Preset.ReactMonorepo || options.preset === Preset.ReactNative || - options.preset === Preset.RemixMonorepo) && + options.preset === Preset.RemixMonorepo || + options.preset === Preset.VueMonorepo || + options.preset === Preset.Nuxt || + options.preset === Preset.NodeMonorepo || + options.preset === Preset.Express) && options.workspaces) ) { const workspaces = options.workspaceGlobs ?? ['packages/**']; diff --git a/packages/workspace/src/generators/preset/preset.ts b/packages/workspace/src/generators/preset/preset.ts index d5f633d91444e..37564d7b1c47e 100644 --- a/packages/workspace/src/generators/preset/preset.ts +++ b/packages/workspace/src/generators/preset/preset.ts @@ -135,6 +135,8 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'playwright', addPlugin, nxCloudToken: options.nxCloudToken, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.VueStandalone) { const { applicationGenerator: vueApplicationGenerator } = require('@nx' + @@ -163,6 +165,8 @@ async function createPreset(tree: Tree, options: Schema) { e2eTestRunner: options.e2eTestRunner ?? 'playwright', addPlugin, nxCloudToken: options.nxCloudToken, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.NuxtStandalone) { const { applicationGenerator: nuxtApplicationGenerator } = require('@nx' + @@ -234,6 +238,8 @@ async function createPreset(tree: Tree, options: Schema) { linter: options.linter, e2eTestRunner: options.e2eTestRunner ?? 'jest', addPlugin, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.Express) { const { @@ -245,6 +251,8 @@ async function createPreset(tree: Tree, options: Schema) { linter: options.linter, e2eTestRunner: options.e2eTestRunner ?? 'jest', addPlugin, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else if (options.preset === Preset.ReactNative) { const { reactNativeApplicationGenerator } = require('@nx' + @@ -322,6 +330,8 @@ async function createPreset(tree: Tree, options: Schema) { rootProject: false, e2eTestRunner: options.e2eTestRunner ?? 'jest', addPlugin, + useTsSolution: options.workspaces, + formatter: options.formatter, }); } else { throw new Error(`Invalid preset ${options.preset}`);