diff --git a/docs/generated/packages/js/executors/tsc.json b/docs/generated/packages/js/executors/tsc.json index 2df446a961..5c295f7dae 100644 --- a/docs/generated/packages/js/executors/tsc.json +++ b/docs/generated/packages/js/executors/tsc.json @@ -20,13 +20,13 @@ "generateExportsField": { "type": "boolean", "alias": "exports", - "description": "Update the output package.json file's 'exports' field. This field is used by Node and bundles.", + "description": "Update the output package.json file's 'exports' field. This field is used by Node and bundlers. Ignored when `generatePackageJson` is set to `false`.", "default": false, "x-priority": "important" }, "additionalEntryPoints": { "type": "array", - "description": "Additional entry-points to add to exports field in the package.json file.", + "description": "Additional entry-points to add to exports field in the package.json file. Ignored when `generatePackageJson` is set to `false`.", "items": { "type": "string" }, "x-priority": "important" }, @@ -132,9 +132,14 @@ }, "generateLockfile": { "type": "boolean", - "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", + "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match. Ignored when `generatePackageJson` is set to `false`.", "default": false, "x-priority": "internal" + }, + "generatePackageJson": { + "type": "boolean", + "description": "Generate package.json file in the output folder.", + "default": true } }, "required": ["main", "outputPath", "tsConfig"], diff --git a/docs/generated/packages/plugin/generators/create-package.json b/docs/generated/packages/plugin/generators/create-package.json index b0fb581ca1..e2ecc9d1f9 100644 --- a/docs/generated/packages/plugin/generators/create-package.json +++ b/docs/generated/packages/plugin/generators/create-package.json @@ -1,6 +1,6 @@ { "name": "create-package", - "factory": "./src/generators/create-package/create-package", + "factory": "./src/generators/create-package/create-package#createPackageGeneratorInternal", "schema": { "$schema": "https://json-schema.org/schema", "cli": "nx", @@ -31,15 +31,13 @@ }, "unitTestRunner": { "type": "string", - "enum": ["jest", "none"], - "description": "Test runner to use for unit tests.", - "default": "jest" + "enum": ["none", "jest"], + "description": "Test runner to use for unit tests." }, "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["none", "eslint"] }, "tags": { "type": "string", @@ -62,13 +60,17 @@ "type": "string", "description": "The name of the e2e project.", "x-prompt": "What is the name of the e2e project? Leave blank to skip e2e tests" + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["directory", "name", "project"], "presets": [] }, "description": "Create a package which can be used by npx to create a new workspace", - "implementation": "/packages/plugin/src/generators/create-package/create-package.ts", + "implementation": "/packages/plugin/src/generators/create-package/create-package#createPackageGeneratorInternal.ts", "aliases": [], "hidden": false, "path": "/packages/plugin/src/generators/create-package/schema.json", diff --git a/docs/generated/packages/plugin/generators/e2e-project.json b/docs/generated/packages/plugin/generators/e2e-project.json index 32c73bff79..09d118b519 100644 --- a/docs/generated/packages/plugin/generators/e2e-project.json +++ b/docs/generated/packages/plugin/generators/e2e-project.json @@ -33,8 +33,7 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint", "none"], - "default": "eslint" + "enum": ["none", "eslint"] }, "minimal": { "type": "boolean", @@ -46,6 +45,10 @@ "type": "boolean", "default": false, "x-priority": "internal" + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["pluginName", "npmPackageName"], diff --git a/docs/generated/packages/plugin/generators/plugin.json b/docs/generated/packages/plugin/generators/plugin.json index 8035a100da..e8c594a6b4 100644 --- a/docs/generated/packages/plugin/generators/plugin.json +++ b/docs/generated/packages/plugin/generators/plugin.json @@ -1,6 +1,6 @@ { "name": "plugin", - "factory": "./src/generators/plugin/plugin", + "factory": "./src/generators/plugin/plugin#pluginGeneratorInternal", "schema": { "$schema": "https://json-schema.org/schema", "cli": "nx", @@ -34,14 +34,14 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["none", "eslint"], + "x-priority": "important" }, "unitTestRunner": { - "type": "string", - "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "type": "string", + "enum": ["none", "jest"], + "x-priority": "important" }, "tags": { "type": "string", @@ -92,13 +92,17 @@ "type": "boolean", "description": "Generates a boilerplate for publishing the plugin to npm.", "default": false + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["directory"], "presets": [] }, "description": "Create a Nx Plugin.", - "implementation": "/packages/plugin/src/generators/plugin/plugin.ts", + "implementation": "/packages/plugin/src/generators/plugin/plugin#pluginGeneratorInternal.ts", "aliases": [], "hidden": false, "path": "/packages/plugin/src/generators/plugin/schema.json", diff --git a/docs/generated/packages/plugin/generators/preset.json b/docs/generated/packages/plugin/generators/preset.json index 0d1ce660e5..ed7ffdc264 100644 --- a/docs/generated/packages/plugin/generators/preset.json +++ b/docs/generated/packages/plugin/generators/preset.json @@ -1,6 +1,6 @@ { "name": "preset", - "factory": "./src/generators/preset/generator", + "factory": "./src/generators/preset/generator#presetGeneratorInternal", "schema": { "$schema": "https://json-schema.org/schema", "cli": "nx", @@ -27,6 +27,10 @@ "createPackageName": { "type": "string", "description": "Name of package which creates a workspace" + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["pluginName"], @@ -35,7 +39,7 @@ "description": "Initializes a workspace with an nx-plugin inside of it. Use as: `create-nx-workspace --preset @nx/plugin`.", "hidden": true, "x-use-standalone-layout": true, - "implementation": "/packages/plugin/src/generators/preset/generator.ts", + "implementation": "/packages/plugin/src/generators/preset/generator#presetGeneratorInternal.ts", "aliases": [], "path": "/packages/plugin/src/generators/preset/schema.json", "type": "generator" diff --git a/e2e/plugin/src/nx-plugin.test.ts b/e2e/plugin/src/nx-plugin.test.ts index 01e09e67d0..d1b2f0eefd 100644 --- a/e2e/plugin/src/nx-plugin.test.ts +++ b/e2e/plugin/src/nx-plugin.test.ts @@ -60,7 +60,7 @@ describe('Nx Plugin', () => { runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`); runCLI( - `generate @nx/plugin:migration --path=${plugin}/src/migrations/update-${version} --packageVersion=${version} --packageJsonUpdates=false` + `generate @nx/plugin:migration --path=${plugin}/src/migrations/update-${version}/update-${version} --packageVersion=${version} --packageJsonUpdates=false` ); const lintResults = runCLI(`lint ${plugin}`); @@ -92,7 +92,7 @@ describe('Nx Plugin', () => { runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`); runCLI( - `generate @nx/plugin:generator ${plugin}/src/generators/${generator} --name ${generator}` + `generate @nx/plugin:generator ${plugin}/src/generators/${generator}/generator --name ${generator}` ); const lintResults = runCLI(`lint ${plugin}`); @@ -129,7 +129,7 @@ describe('Nx Plugin', () => { runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`); runCLI( - `generate @nx/plugin:executor --name ${executor} --path=${plugin}/src/executors/${executor} --includeHasher` + `generate @nx/plugin:executor --name ${executor} --path=${plugin}/src/executors/${executor}/executor --includeHasher` ); const lintResults = runCLI(`lint ${plugin}`); @@ -178,19 +178,19 @@ describe('Nx Plugin', () => { runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`); runCLI( - `generate @nx/plugin:generator --name=${goodGenerator} --path=${plugin}/src/generators/${goodGenerator}` + `generate @nx/plugin:generator --name=${goodGenerator} --path=${plugin}/src/generators/${goodGenerator}/generator` ); runCLI( - `generate @nx/plugin:generator --name=${badFactoryPath} --path=${plugin}/src/generators/${badFactoryPath}` + `generate @nx/plugin:generator --name=${badFactoryPath} --path=${plugin}/src/generators/${badFactoryPath}/generator` ); runCLI( - `generate @nx/plugin:executor --name=${goodExecutor} --path=${plugin}/src/executors/${goodExecutor}` + `generate @nx/plugin:executor --name=${goodExecutor} --path=${plugin}/src/executors/${goodExecutor}/executor` ); runCLI( - `generate @nx/plugin:executor --name=${badExecutorBadImplPath} --path=${plugin}/src/executors/${badExecutorBadImplPath}` + `generate @nx/plugin:executor --name=${badExecutorBadImplPath} --path=${plugin}/src/executors/${badExecutorBadImplPath}/executor` ); runCLI( @@ -308,11 +308,11 @@ describe('Nx Plugin', () => { const generatedProject = uniq('project'); runCLI( - `generate @nx/plugin:generator --name ${generator} --path ${plugin}/src/generators/${generator}` + `generate @nx/plugin:generator --name ${generator} --path ${plugin}/src/generators/${generator}/generator` ); runCLI( - `generate @nx/plugin:executor --name ${executor} --path ${plugin}/src/executors/${executor}` + `generate @nx/plugin:executor --name ${executor} --path ${plugin}/src/executors/${executor}/executor` ); updateFile( @@ -349,7 +349,7 @@ describe('Nx Plugin', () => { expect(() => { runCLI( - `generate @nx/plugin:generator ${plugin}/src/generators/${generator} --name ${generator}` + `generate @nx/plugin:generator ${plugin}/src/generators/${generator}/generator --name ${generator}` ); runCLI( diff --git a/packages/cypress/src/generators/configuration/configuration.ts b/packages/cypress/src/generators/configuration/configuration.ts index c93aae1368..096296f4e2 100644 --- a/packages/cypress/src/generators/configuration/configuration.ts +++ b/packages/cypress/src/generators/configuration/configuration.ts @@ -20,12 +20,12 @@ import { writeJson, } from '@nx/devkit'; import { resolveImportPath } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; import { Linter, LinterType } from '@nx/eslint'; import { getRelativePathToRootTsConfig, initGenerator as jsInitGenerator, } from '@nx/js'; +import { normalizeLinterOption } from '@nx/js/src/utils/generator-prompts'; import { getProjectPackageManagerWorkspaceState, getProjectPackageManagerWorkspaceStateWarningTask, @@ -165,28 +165,7 @@ function ensureDependencies(tree: Tree, options: NormalizedSchema) { } async function normalizeOptions(tree: Tree, options: CypressE2EConfigSchema) { - const isTsSolutionSetup = isUsingTsSolutionSetup(tree); - - let linter = options.linter; - if (!linter) { - const choices = isTsSolutionSetup - ? [{ name: 'none' }, { name: 'eslint' }] - : [{ name: 'eslint' }, { name: 'none' }]; - const defaultValue = isTsSolutionSetup ? 'none' : 'eslint'; - - linter = await promptWhenInteractive<{ - linter: 'none' | 'eslint'; - }>( - { - type: 'select', - name: 'linter', - message: `Which linter would you like to use?`, - choices, - initial: 0, - }, - { linter: defaultValue } - ).then(({ linter }) => linter); - } + const linter = await normalizeLinterOption(tree, options.linter); const projectConfig: ProjectConfiguration | undefined = readProjectConfiguration(tree, options.project); diff --git a/packages/jest/src/generators/configuration/configuration.ts b/packages/jest/src/generators/configuration/configuration.ts index e448819cee..a294ec79c9 100644 --- a/packages/jest/src/generators/configuration/configuration.ts +++ b/packages/jest/src/generators/configuration/configuration.ts @@ -1,17 +1,14 @@ import { formatFiles, GeneratorCallback, - output, - readJson, + logger, readNxJson, readProjectConfiguration, runTasksInSerial, Tree, } from '@nx/devkit'; -import { - getRootTsConfigFileName, - initGenerator as jsInitGenerator, -} from '@nx/js'; +import { initGenerator as jsInitGenerator } from '@nx/js'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { JestPluginOptions } from '../../plugins/plugin'; import { getPresetExt } from '../../utils/config/config-file'; import { jestInitGenerator } from '../init/init'; @@ -74,6 +71,7 @@ function normalizeOptions( ...schemaDefaults, ...options, rootProject: project.root === '.' || project.root === '', + isTsSolutionSetup: isUsingTsSolutionSetup(tree), }; } @@ -115,10 +113,15 @@ export async function configurationGeneratorInternal( ); } }); + if (!hasPlugin || options.addExplicitTargets) { updateWorkspace(tree, options); } + if (options.isTsSolutionSetup) { + ignoreTestOutput(tree); + } + if (!schema.skipFormat) { await formatFiles(tree); } @@ -126,4 +129,18 @@ export async function configurationGeneratorInternal( return runTasksInSerial(...tasks); } +function ignoreTestOutput(tree: Tree): void { + if (!tree.exists('.gitignore')) { + logger.warn(`Couldn't find a root .gitignore file to update.`); + } + + let content = tree.read('.gitignore', 'utf-8'); + if (/^test-output$/gm.test(content)) { + return; + } + + content = `${content}\ntest-output\n`; + tree.write('.gitignore', content); +} + export default configurationGenerator; diff --git a/packages/jest/src/generators/configuration/files-angular/jest.config.ts__tmpl__ b/packages/jest/src/generators/configuration/files-angular/jest.config.ts__tmpl__ index d9e185b331..8e1ed3de6d 100644 --- a/packages/jest/src/generators/configuration/files-angular/jest.config.ts__tmpl__ +++ b/packages/jest/src/generators/configuration/files-angular/jest.config.ts__tmpl__ @@ -3,7 +3,7 @@ preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>', setupFilesAfterEnv: ['/src/test-setup.ts'],<% if(testEnvironment) { %> testEnvironment: '<%= testEnvironment %>',<% } %> - coverageDirectory: '<%= offsetFromRoot %>coverage/<%= projectRoot %>', + coverageDirectory: '<%= coverageDirectory %>', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', diff --git a/packages/jest/src/generators/configuration/files-angular/tsconfig.spec.json__tmpl__ b/packages/jest/src/generators/configuration/files-angular/tsconfig.spec.json__tmpl__ index 3307b9d362..233335dec8 100644 --- a/packages/jest/src/generators/configuration/files-angular/tsconfig.spec.json__tmpl__ +++ b/packages/jest/src/generators/configuration/files-angular/tsconfig.spec.json__tmpl__ @@ -1,8 +1,8 @@ { - "extends": "./tsconfig.json", + "extends": "<%= extendedConfig %>", "compilerOptions": { - "outDir": "<%= offsetFromRoot %>dist/out-tsc", - "module": "commonjs", + "outDir": "<%= outDir %>",<% if (module) { %> + "module": "<%= module %>",<% } %> "target": "es2016", "types": ["jest", "node"] },<% if(setupFile !== 'none') { %> diff --git a/packages/jest/src/generators/configuration/files/jest.config.ts__tmpl__ b/packages/jest/src/generators/configuration/files/jest.config.ts__tmpl__ index 1117faf173..71cb13fe9e 100644 --- a/packages/jest/src/generators/configuration/files/jest.config.ts__tmpl__ +++ b/packages/jest/src/generators/configuration/files/jest.config.ts__tmpl__ @@ -7,7 +7,7 @@ <% if (supportTsx){ %>'^.+\\.[tj]sx?$'<% } else { %>'^.+\\.[tj]s$'<% } %>: <% if (transformerOptions) { %>['<%= transformer %>', <%- transformerOptions %>]<% } else { %>'<%= transformer %>'<% } %> }, <% if (supportTsx) { %>moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],<% } else { %>moduleFileExtensions: ['ts', 'js', 'html'],<% } %><% } %> - coverageDirectory: '<%= offsetFromRoot %>coverage/<%= projectRoot %>'<% if(rootProject){ %>, + coverageDirectory: '<%= coverageDirectory %>'<% if(rootProject){ %>, testMatch: [ '/src/**/__tests__/**/*.[jt]s?(x)', '/src/**/*(*.)@(spec|test).[jt]s?(x)', diff --git a/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ b/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ index d6e9ff767e..0e758a58c5 100644 --- a/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ +++ b/packages/jest/src/generators/configuration/files/tsconfig.spec.json__tmpl__ @@ -1,8 +1,8 @@ { - "extends": "./tsconfig.json", + "extends": "<%= extendedConfig %>", "compilerOptions": { - "outDir": "<%= offsetFromRoot %>dist/out-tsc", - "module": "commonjs", + "outDir": "<%= outDir %>",<% if (module) { %> + "module": "<%= module %>",<% } %> "types": ["jest", "node"] },<% if(setupFile !== 'none') { %> "files": ["src/test-setup.ts"],<% } %> diff --git a/packages/jest/src/generators/configuration/lib/create-files.ts b/packages/jest/src/generators/configuration/lib/create-files.ts index da718b7d89..847501f931 100644 --- a/packages/jest/src/generators/configuration/lib/create-files.ts +++ b/packages/jest/src/generators/configuration/lib/create-files.ts @@ -4,6 +4,7 @@ import { readProjectConfiguration, Tree, } from '@nx/devkit'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { join } from 'path'; import type { JestPresetExtension } from '../../../utils/config/config-file'; import { NormalizedJestProjectSchema } from '../schema'; @@ -33,6 +34,12 @@ export function createFiles( transformerOptions = "{ tsconfig: '/tsconfig.spec.json' }"; } + const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + + const projectRoot = options.rootProject + ? options.project + : projectConfig.root; + const rootOffset = offsetFromRoot(projectConfig.root); generateFiles(tree, join(__dirname, filesFolder), projectConfig.root, { tmpl: '', ...options, @@ -45,9 +52,17 @@ export function createFiles( transformerOptions, js: !!options.js, rootProject: options.rootProject, - projectRoot: options.rootProject ? options.project : projectConfig.root, - offsetFromRoot: offsetFromRoot(projectConfig.root), + projectRoot, + offsetFromRoot: rootOffset, presetExt, + coverageDirectory: isTsSolutionSetup + ? `test-output/jest/coverage` + : `${rootOffset}coverage/${projectRoot}`, + extendedConfig: isTsSolutionSetup + ? `${rootOffset}tsconfig.base.json` + : './tsconfig.json', + outDir: isTsSolutionSetup ? `./out-tsc/jest` : `${rootOffset}dist/out-tsc`, + module: !isTsSolutionSetup ? 'commonjs' : undefined, }); if (options.setupFile === 'none') { diff --git a/packages/jest/src/generators/configuration/lib/update-workspace.ts b/packages/jest/src/generators/configuration/lib/update-workspace.ts index 854e13f41f..6218a4cc24 100644 --- a/packages/jest/src/generators/configuration/lib/update-workspace.ts +++ b/packages/jest/src/generators/configuration/lib/update-workspace.ts @@ -19,9 +19,13 @@ export function updateWorkspace( projectConfig.targets[options.targetName] = { executor: '@nx/jest:jest', outputs: [ - options.rootProject - ? joinPathFragments('{workspaceRoot}', 'coverage', '{projectName}') - : joinPathFragments('{workspaceRoot}', 'coverage', '{projectRoot}'), + options.isTsSolutionSetup + ? '{projectRoot}/test-output/jest/coverage' + : joinPathFragments( + '{workspaceRoot}', + 'coverage', + options.rootProject ? '{projectName}' : '{projectRoot}' + ), ], options: { jestConfig: joinPathFragments( diff --git a/packages/jest/src/generators/configuration/schema.d.ts b/packages/jest/src/generators/configuration/schema.d.ts index 68f8ceaea0..e8daf83f66 100644 --- a/packages/jest/src/generators/configuration/schema.d.ts +++ b/packages/jest/src/generators/configuration/schema.d.ts @@ -29,4 +29,5 @@ export interface JestProjectSchema { export type NormalizedJestProjectSchema = JestProjectSchema & { rootProject: boolean; + isTsSolutionSetup: boolean; }; diff --git a/packages/js/src/executors/tsc/lib/normalize-options.ts b/packages/js/src/executors/tsc/lib/normalize-options.ts index 6f04b34b9f..ce7f15eab2 100644 --- a/packages/js/src/executors/tsc/lib/normalize-options.ts +++ b/packages/js/src/executors/tsc/lib/normalize-options.ts @@ -57,5 +57,6 @@ export function normalizeOptions( outputPath, options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js') ), + generatePackageJson: options.generatePackageJson ?? true, }; } diff --git a/packages/js/src/executors/tsc/schema.json b/packages/js/src/executors/tsc/schema.json index 77cc8490b8..e8a24e1ebc 100644 --- a/packages/js/src/executors/tsc/schema.json +++ b/packages/js/src/executors/tsc/schema.json @@ -16,13 +16,13 @@ "generateExportsField": { "type": "boolean", "alias": "exports", - "description": "Update the output package.json file's 'exports' field. This field is used by Node and bundles.", + "description": "Update the output package.json file's 'exports' field. This field is used by Node and bundlers. Ignored when `generatePackageJson` is set to `false`.", "default": false, "x-priority": "important" }, "additionalEntryPoints": { "type": "array", - "description": "Additional entry-points to add to exports field in the package.json file.", + "description": "Additional entry-points to add to exports field in the package.json file. Ignored when `generatePackageJson` is set to `false`.", "items": { "type": "string" }, @@ -103,9 +103,14 @@ }, "generateLockfile": { "type": "boolean", - "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match.", + "description": "Generate a lockfile (e.g. package-lock.json) that matches the workspace lockfile to ensure package versions match. Ignored when `generatePackageJson` is set to `false`.", "default": false, "x-priority": "internal" + }, + "generatePackageJson": { + "type": "boolean", + "description": "Generate package.json file in the output folder.", + "default": true } }, "required": ["main", "outputPath", "tsConfig"], diff --git a/packages/js/src/executors/tsc/tsc.impl.ts b/packages/js/src/executors/tsc/tsc.impl.ts index 9cf51138b5..777fc219aa 100644 --- a/packages/js/src/executors/tsc/tsc.impl.ts +++ b/packages/js/src/executors/tsc/tsc.impl.ts @@ -114,19 +114,21 @@ export async function* tscExecutor( tsCompilationOptions, async () => { await assetHandler.processAllAssetsOnce(); - updatePackageJson( - { - ...options, - additionalEntryPoints: createEntryPoints( - options.additionalEntryPoints, - context.root - ), - format: [determineModuleFormatFromTsConfig(options.tsConfig)], - }, - context, - target, - dependencies - ); + if (options.generatePackageJson) { + updatePackageJson( + { + ...options, + additionalEntryPoints: createEntryPoints( + options.additionalEntryPoints, + context.root + ), + format: [determineModuleFormatFromTsConfig(options.tsConfig)], + }, + context, + target, + dependencies + ); + } postProcessInlinedDependencies( tsCompilationOptions.outputPath, tsCompilationOptions.projectRoot, @@ -145,29 +147,32 @@ export async function* tscExecutor( if (isDaemonEnabled() && options.watch) { const disposeWatchAssetChanges = await assetHandler.watchAndProcessOnAssetChange(); - const disposePackageJsonChanges = await watchForSingleFileChanges( - context.projectName, - options.projectRoot, - 'package.json', - () => - updatePackageJson( - { - ...options, - additionalEntryPoints: createEntryPoints( - options.additionalEntryPoints, - context.root - ), - format: [determineModuleFormatFromTsConfig(options.tsConfig)], - }, - context, - target, - dependencies - ) - ); + let disposePackageJsonChanges: undefined | (() => void); + if (options.generatePackageJson) { + disposePackageJsonChanges = await watchForSingleFileChanges( + context.projectName, + options.projectRoot, + 'package.json', + () => + updatePackageJson( + { + ...options, + additionalEntryPoints: createEntryPoints( + options.additionalEntryPoints, + context.root + ), + format: [determineModuleFormatFromTsConfig(options.tsConfig)], + }, + context, + target, + dependencies + ) + ); + } const handleTermination = async (exitCode: number) => { await typescriptCompilation.close(); disposeWatchAssetChanges(); - disposePackageJsonChanges(); + disposePackageJsonChanges?.(); process.exit(exitCode); }; process.on('SIGINT', () => handleTermination(128 + 2)); diff --git a/packages/js/src/generators/library/library.spec.ts b/packages/js/src/generators/library/library.spec.ts index ae74a4d8fa..5622a87602 100644 --- a/packages/js/src/generators/library/library.spec.ts +++ b/packages/js/src/generators/library/library.spec.ts @@ -1561,6 +1561,7 @@ describe('lib', () => { "name": "@proj/my-lib", "nx": { "name": "my-lib", + "projectType": "library", "sourceRoot": "my-lib/src", }, "private": true, diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index 98e2ef5a55..022af8f92f 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -33,6 +33,7 @@ import { findMatchingProjects } from 'nx/src/utils/find-matching-projects'; import { type PackageJson } from 'nx/src/utils/package-json'; import { join } from 'path'; import type { CompilerOptions } from 'typescript'; +import { normalizeLinterOption } from '../../utils/generator-prompts'; import { getProjectPackageManagerWorkspaceState, getProjectPackageManagerWorkspaceStateWarningTask, @@ -41,6 +42,10 @@ import { addSwcConfig } from '../../utils/swc/add-swc-config'; import { getSwcDependencies } from '../../utils/swc/add-swc-dependencies'; import { getNeededCompilerOptionOverrides } from '../../utils/typescript/configuration'; import { tsConfigBaseOptions } from '../../utils/typescript/create-ts-config'; +import { + ensureProjectIsExcludedFromPluginRegistrations, + ensureProjectIsIncludedInPluginRegistrations, +} from '../../utils/typescript/plugin'; import { addTsConfigPath, getRelativePathToRootTsConfig, @@ -64,10 +69,6 @@ import type { LibraryGeneratorSchema, NormalizedLibraryGeneratorOptions, } from './schema'; -import { - ensureProjectIsExcludedFromPluginRegistrations, - ensureProjectIsIncludedInPluginRegistrations, -} from '../../utils/typescript/plugin'; const defaultOutputDirectory = 'dist'; @@ -202,16 +203,6 @@ export async function libraryGeneratorInternal( tree, joinPathFragments(options.projectRoot, 'tsconfig.spec.json'), (json) => { - const rootOffset = offsetFromRoot(options.projectRoot); - // ensure it extends from the root tsconfig.base.json - json.extends = joinPathFragments(rootOffset, 'tsconfig.base.json'); - // ensure outDir is set to the correct value - json.compilerOptions ??= {}; - json.compilerOptions.outDir = joinPathFragments( - rootOffset, - 'dist/out-tsc', - options.projectRoot - ); // add project reference to the runtime tsconfig.lib.json file json.references ??= []; json.references.push({ path: './tsconfig.lib.json' }); @@ -225,6 +216,7 @@ export async function libraryGeneratorInternal( } if ( + !options.skipWorkspacesWarning && options.isUsingTsSolutionConfig && options.projectPackageManagerWorkspaceState !== 'included' ) { @@ -278,7 +270,8 @@ async function configureProject( options.config !== 'npm-scripts' && (options.bundler === 'swc' || options.bundler === 'esbuild' || - (!options.isUsingTsSolutionConfig && options.bundler === 'tsc')) + ((!options.isUsingTsSolutionConfig || options.useTscExecutor) && + options.bundler === 'tsc')) ) { const outputPath = getOutputPath(options); const executor = getBuildExecutor(options.bundler); @@ -305,6 +298,8 @@ async function configureProject( if (options.isUsingTsSolutionConfig) { if (options.bundler === 'esbuild') { projectConfiguration.targets.build.options.declarationRootDir = `${options.projectRoot}/src`; + } else if (options.bundler === 'swc') { + projectConfiguration.targets.build.options.stripLeadingPaths = true; } } else { projectConfiguration.targets.build.options.assets = []; @@ -356,8 +351,6 @@ async function configureProject( if (!projectConfiguration.tags?.length) { delete projectConfiguration.tags; } - // automatically inferred as `library` - delete projectConfiguration.projectType; // empty targets are cleaned up automatically by `updateProjectConfiguration` updateProjectConfiguration(tree, options.name, projectConfiguration); @@ -690,23 +683,12 @@ async function normalizeOptions( process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false; + options.linter = await normalizeLinterOption(tree, options.linter); + const hasPlugin = isUsingTypeScriptPlugin(tree); const isUsingTsSolutionConfig = isUsingTsSolutionSetup(tree); if (isUsingTsSolutionConfig) { - options.linter ??= await promptWhenInteractive<{ - linter: 'none' | 'eslint'; - }>( - { - type: 'autocomplete', - name: 'linter', - message: `Which linter would you like to use?`, - choices: [{ name: 'none' }, { name: 'eslint' }], - initial: 0, - }, - { linter: 'none' } - ).then(({ linter }) => linter); - options.unitTestRunner ??= await promptWhenInteractive<{ unitTestRunner: 'none' | 'jest' | 'vitest'; }>( @@ -720,19 +702,6 @@ async function normalizeOptions( { unitTestRunner: 'none' } ).then(({ unitTestRunner }) => unitTestRunner); } else { - options.linter ??= await promptWhenInteractive<{ - linter: 'none' | 'eslint'; - }>( - { - type: 'autocomplete', - name: 'linter', - message: `Which linter would you like to use?`, - choices: [{ name: 'eslint' }, { name: 'none' }], - initial: 0, - }, - { linter: 'eslint' } - ).then(({ linter }) => linter); - options.unitTestRunner ??= await promptWhenInteractive<{ unitTestRunner: 'none' | 'jest' | 'vitest'; }>( @@ -1109,10 +1078,10 @@ function determineEntryFields( return { type: 'commonjs', main: options.isUsingTsSolutionConfig - ? './dist/src/index.js' + ? './dist/index.js' : './src/index.js', typings: options.isUsingTsSolutionConfig - ? './dist/src/index.d.ts' + ? './dist/index.d.ts' : './src/index.d.ts', }; case 'rollup': diff --git a/packages/js/src/generators/library/schema.d.ts b/packages/js/src/generators/library/schema.d.ts index 8b77ff9062..4112a4445d 100644 --- a/packages/js/src/generators/library/schema.d.ts +++ b/packages/js/src/generators/library/schema.d.ts @@ -37,6 +37,8 @@ export interface LibraryGeneratorSchema { simpleName?: boolean; addPlugin?: boolean; useProjectJson?: boolean; + skipWorkspacesWarning?: boolean; + useTscExecutor?: boolean; } export interface NormalizedLibraryGeneratorOptions diff --git a/packages/js/src/utils/add-local-registry-scripts.ts b/packages/js/src/utils/add-local-registry-scripts.ts index e85ca2d937..77ae25d0ef 100644 --- a/packages/js/src/utils/add-local-registry-scripts.ts +++ b/packages/js/src/utils/add-local-registry-scripts.ts @@ -1,12 +1,14 @@ import { output, ProjectConfiguration, readJson, type Tree } from '@nx/devkit'; +import type { PackageJson } from 'nx/src/utils/package-json'; -const startLocalRegistryScript = (localRegistryTarget: string) => ` -/** +const startLocalRegistryScript = (localRegistryTarget: string) => `/** * This script starts a local registry for e2e testing purposes. * It is meant to be called in jest's globalSetup. */ + +/// + import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry'; -import { execFileSync } from 'child_process'; import { releasePublish, releaseVersion } from 'nx/release'; export default async () => { @@ -38,12 +40,13 @@ export default async () => { }; `; -const stopLocalRegistryScript = ` -/** +const stopLocalRegistryScript = `/** * This script stops the local registry for e2e testing purposes. * It is meant to be called in jest's globalTeardown. */ +/// + export default () => { if (global.stopLocalRegistry) { global.stopLocalRegistry(); @@ -51,16 +54,27 @@ export default () => { }; `; +const registryDeclarationText = `declare function stopLocalRegistry(): void; +`; + export function addLocalRegistryScripts(tree: Tree) { const startLocalRegistryPath = 'tools/scripts/start-local-registry.ts'; const stopLocalRegistryPath = 'tools/scripts/stop-local-registry.ts'; + const registryDeclarationPath = 'tools/scripts/registry.d.ts'; - const projectConfiguration: ProjectConfiguration = readJson( - tree, - 'project.json' - ); + let projectName: string; + try { + ({ name: projectName } = readJson( + tree, + 'project.json' + )); + } catch { + // if project.json doesn't exist, try package.json + const { name, nx } = readJson(tree, 'package.json'); + projectName = nx?.name ?? name; + } - const localRegistryTarget = `${projectConfiguration.name}:local-registry`; + const localRegistryTarget = `${projectName}:local-registry`; if (!tree.exists(startLocalRegistryPath)) { tree.write( startLocalRegistryPath, @@ -80,6 +94,9 @@ export function addLocalRegistryScripts(tree: Tree) { if (!tree.exists(stopLocalRegistryPath)) { tree.write(stopLocalRegistryPath, stopLocalRegistryScript); } + if (!tree.exists(registryDeclarationPath)) { + tree.write(registryDeclarationPath, registryDeclarationText); + } return { startLocalRegistryPath, stopLocalRegistryPath }; } diff --git a/packages/js/src/utils/generator-prompts.ts b/packages/js/src/utils/generator-prompts.ts new file mode 100644 index 0000000000..a54a80c984 --- /dev/null +++ b/packages/js/src/utils/generator-prompts.ts @@ -0,0 +1,62 @@ +import type { Tree } from '@nx/devkit'; +import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; +import { isUsingTsSolutionSetup } from './typescript/ts-solution-setup'; + +export async function normalizeLinterOption( + tree: Tree, + linter: undefined | 'none' | 'eslint' +): Promise<'none' | 'eslint'> { + if (linter) { + return linter; + } + + const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + const choices = isTsSolutionSetup + ? [{ name: 'none' }, { name: 'eslint' }] + : [{ name: 'eslint' }, { name: 'none' }]; + const defaultValue = isTsSolutionSetup ? 'none' : 'eslint'; + + return await promptWhenInteractive<{ + linter: 'none' | 'eslint'; + }>( + { + type: 'autocomplete', + name: 'linter', + message: `Which linter would you like to use?`, + choices, + initial: 0, + }, + { linter: defaultValue } + ).then(({ linter }) => linter); +} + +export async function normalizeUnitTestRunnerOption< + T extends 'none' | 'jest' | 'vitest' +>( + tree: Tree, + unitTestRunner: undefined | T, + testRunners: Array<'jest' | 'vitest'> = ['jest', 'vitest'] +): Promise { + if (unitTestRunner) { + return unitTestRunner; + } + + const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + const choices = isTsSolutionSetup + ? [{ name: 'none' }, ...testRunners.map((runner) => ({ name: runner }))] + : [...testRunners.map((runner) => ({ name: runner })), { name: 'none' }]; + const defaultValue = (isTsSolutionSetup ? 'none' : testRunners[0]) as T; + + return await promptWhenInteractive<{ + unitTestRunner: T; + }>( + { + type: 'autocomplete', + name: 'unitTestRunner', + message: `Which unit test runner would you like to use?`, + choices, + initial: 0, + }, + { unitTestRunner: defaultValue } + ).then(({ unitTestRunner }) => unitTestRunner); +} diff --git a/packages/js/src/utils/schema.d.ts b/packages/js/src/utils/schema.d.ts index a492eaca9c..06d9c7f762 100644 --- a/packages/js/src/utils/schema.d.ts +++ b/packages/js/src/utils/schema.d.ts @@ -19,12 +19,14 @@ export interface ExecutorOptions { externalBuildTargets?: string[]; generateLockfile?: boolean; stripLeadingPaths?: boolean; + generatePackageJson?: boolean; } export interface NormalizedExecutorOptions extends ExecutorOptions { rootDir: string; projectRoot: string; mainOutputPath: string; + generatePackageJson: boolean; files: Array; root?: string; sourceRoot?: string; diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index 56a99a6f92..3e46cc2dff 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -42,6 +42,8 @@ export interface PackageJson { type?: 'module' | 'commonjs'; main?: string; types?: string; + // interchangeable with `types`: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#including-declarations-in-your-npm-package + typings?: string; module?: string; exports?: | string diff --git a/packages/playwright/src/generators/configuration/configuration.ts b/packages/playwright/src/generators/configuration/configuration.ts index 0e78d719c3..dab57e23d9 100644 --- a/packages/playwright/src/generators/configuration/configuration.ts +++ b/packages/playwright/src/generators/configuration/configuration.ts @@ -20,8 +20,8 @@ import { writeJson, } from '@nx/devkit'; import { resolveImportPath } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { promptWhenInteractive } from '@nx/devkit/src/generators/prompt'; import { getRelativePathToRootTsConfig } from '@nx/js'; +import { normalizeLinterOption } from '@nx/js/src/utils/generator-prompts'; import { getProjectPackageManagerWorkspaceState, getProjectPackageManagerWorkspaceStateWarningTask, @@ -216,36 +216,7 @@ async function normalizeOptions( (process.env.NX_ADD_PLUGINS !== 'false' && nxJson.useInferencePlugins !== false); - const isTsSolutionSetup = isUsingTsSolutionSetup(tree); - - let linter = options.linter; - if (isTsSolutionSetup) { - linter ??= await promptWhenInteractive<{ - linter: 'none' | 'eslint'; - }>( - { - type: 'autocomplete', - name: 'linter', - message: `Which linter would you like to use?`, - choices: [{ name: 'none' }, { name: 'eslint' }], - initial: 0, - }, - { linter: 'none' } - ).then(({ linter }) => linter); - } else { - linter ??= await promptWhenInteractive<{ - linter: 'none' | 'eslint'; - }>( - { - type: 'autocomplete', - name: 'linter', - message: `Which linter would you like to use?`, - choices: [{ name: 'eslint' }, { name: 'none' }], - initial: 0, - }, - { linter: 'eslint' } - ).then(({ linter }) => linter); - } + const linter = await normalizeLinterOption(tree, options.linter); return { ...options, diff --git a/packages/plugin/generators.json b/packages/plugin/generators.json index 2c6592e2ab..d017bf45d2 100644 --- a/packages/plugin/generators.json +++ b/packages/plugin/generators.json @@ -4,12 +4,12 @@ "extends": ["@nx/workspace"], "generators": { "plugin": { - "factory": "./src/generators/plugin/plugin", + "factory": "./src/generators/plugin/plugin#pluginGeneratorInternal", "schema": "./src/generators/plugin/schema.json", "description": "Create a Nx Plugin." }, "create-package": { - "factory": "./src/generators/create-package/create-package", + "factory": "./src/generators/create-package/create-package#createPackageGeneratorInternal", "schema": "./src/generators/create-package/schema.json", "description": "Create a package which can be used by npx to create a new workspace" }, @@ -39,7 +39,7 @@ "description": "Adds linting configuration to validate common json files for nx plugins." }, "preset": { - "factory": "./src/generators/preset/generator", + "factory": "./src/generators/preset/generator#presetGeneratorInternal", "schema": "./src/generators/preset/schema.json", "description": "Initializes a workspace with an nx-plugin inside of it. Use as: `create-nx-workspace --preset @nx/plugin`.", "hidden": true, diff --git a/packages/plugin/src/generators/create-package/create-package.ts b/packages/plugin/src/generators/create-package/create-package.ts index ac82acfcb3..f6966037b5 100644 --- a/packages/plugin/src/generators/create-package/create-package.ts +++ b/packages/plugin/src/generators/create-package/create-package.ts @@ -13,22 +13,36 @@ import { updateProjectConfiguration, } from '@nx/devkit'; import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; +import { + getProjectPackageManagerWorkspaceState, + getProjectPackageManagerWorkspaceStateWarningTask, +} from '@nx/js/src/utils/package-manager-workspaces'; import { addTsLibDependencies } from '@nx/js/src/utils/typescript/add-tslib-dependencies'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { tsLibVersion } from '@nx/js/src/utils/versions'; +import type { PackageJson } from 'nx/src/utils/package-json'; import { nxVersion } from 'nx/src/utils/versions'; -import generatorGenerator from '../generator/generator'; +import { join } from 'path'; +import { hasGenerator } from '../../utils/has-generator'; +import { generatorGenerator } from '../generator/generator'; import { CreatePackageSchema } from './schema'; import { NormalizedSchema, normalizeSchema } from './utils/normalize-schema'; -import { hasGenerator } from '../../utils/has-generator'; -import { join } from 'path'; -import { tsLibVersion } from '@nx/js/src/utils/versions'; export async function createPackageGenerator( host: Tree, schema: CreatePackageSchema ) { - assertNotUsingTsSolutionSetup(host, 'plugin', 'create-package'); + return await createPackageGeneratorInternal(host, { + useProjectJson: true, + addPlugin: false, + ...schema, + }); +} +export async function createPackageGeneratorInternal( + host: Tree, + schema: CreatePackageSchema +) { const tasks: GeneratorCallback[] = []; const options = await normalizeSchema(host, schema); @@ -56,6 +70,20 @@ export async function createPackageGenerator( await formatFiles(host); } + if (options.isTsSolutionSetup) { + const projectPackageManagerWorkspaceState = + getProjectPackageManagerWorkspaceState(host, options.projectRoot); + + if (projectPackageManagerWorkspaceState !== 'included') { + tasks.push( + getProjectPackageManagerWorkspaceStateWarningTask( + projectPackageManagerWorkspaceState, + host.root + ) + ); + } + } + return runTasksInSerial(...tasks); } @@ -73,9 +101,10 @@ async function addPresetGenerator( if (!hasGenerator(host, schema.project, 'preset')) { await generatorGenerator(host, { name: 'preset', - path: join(projectRoot, 'src/generators/preset'), + path: join(projectRoot, 'src/generators/preset/generator'), unitTestRunner: schema.unitTestRunner, skipFormat: true, + skipLintChecks: schema.linter === 'none', }); } @@ -97,18 +126,30 @@ async function createCliPackage( importPath: options.name, skipFormat: true, skipTsConfig: true, + useTscExecutor: true, + skipWorkspacesWarning: true, }); host.delete(joinPathFragments(options.projectRoot, 'src')); + const isTsSolutionSetup = isUsingTsSolutionSetup(host); + // Add the bin entry to the package.json - updateJson( + updateJson( host, joinPathFragments(options.projectRoot, 'package.json'), (packageJson) => { packageJson.bin = { [options.name]: './bin/index.js', }; + if (isTsSolutionSetup) { + packageJson.bin[options.name] = './dist/bin/index.js'; + // this package only exposes a binary entry point and no JS programmatic API + delete packageJson.main; + delete packageJson.types; + delete packageJson.typings; + delete packageJson.exports; + } packageJson.dependencies = { 'create-nx-workspace': nxVersion, ...(options.bundler === 'tsc' && { tslib: tsLibVersion }), @@ -131,14 +172,23 @@ async function createCliPackage( 'bin/index.ts' ); projectConfiguration.implicitDependencies = [options.project]; + if (options.isTsSolutionSetup) { + if (options.bundler === 'tsc') { + projectConfiguration.targets.build.options.generatePackageJson = false; + } else if (options.bundler === 'swc') { + delete projectConfiguration.targets.build.options.stripLeadingPaths; + } + } updateProjectConfiguration(host, options.projectName, projectConfiguration); - // Add bin files to tsconfg.lib.json + // Add bin files and update rootDir in tsconfg.lib.json updateJson( host, joinPathFragments(options.projectRoot, 'tsconfig.lib.json'), (tsConfig) => { tsConfig.include.push('bin/**/*.ts'); + tsConfig.compilerOptions ??= {}; + tsConfig.compilerOptions.rootDir = '.'; return tsConfig; } ); diff --git a/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ b/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ index 998ba0634b..efa72e2602 100644 --- a/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ +++ b/packages/plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ @@ -11,8 +11,9 @@ async function main() { console.log(`Creating the workspace: ${name}`); // This assumes "<%= preset %>" and "<%= projectName %>" are at the same version - // eslint-disable-next-line @typescript-eslint/no-var-requires - const presetVersion = require('../package.json').version; + // eslint-disable-next-line @typescript-eslint/no-var-requires<% if (isTsSolutionSetup) { %> + const presetVersion = require('../../package.json').version;<% } else { %> + const presetVersion = require('../package.json').version;<% } %> // TODO: update below to customize the workspace const { directory } = await createWorkspace( diff --git a/packages/plugin/src/generators/create-package/schema.d.ts b/packages/plugin/src/generators/create-package/schema.d.ts index c82b40925e..925036d541 100644 --- a/packages/plugin/src/generators/create-package/schema.d.ts +++ b/packages/plugin/src/generators/create-package/schema.d.ts @@ -6,12 +6,15 @@ export interface CreatePackageSchema { directory: string; // options to create cli package, passed to js library generator - skipFormat: boolean; + skipFormat?: boolean; tags?: string; - unitTestRunner: 'jest' | 'none'; - linter: Linter | LinterType; - compiler: 'swc' | 'tsc'; + unitTestRunner?: 'jest' | 'none'; + linter?: Linter | LinterType; + compiler?: 'swc' | 'tsc'; // options to create e2e project, passed to e2e project generator e2eProject?: string; + + useProjectJson?: boolean; + addPlugin?: boolean; } diff --git a/packages/plugin/src/generators/create-package/schema.json b/packages/plugin/src/generators/create-package/schema.json index 0a64414121..cdfaf44afa 100644 --- a/packages/plugin/src/generators/create-package/schema.json +++ b/packages/plugin/src/generators/create-package/schema.json @@ -33,15 +33,13 @@ }, "unitTestRunner": { "type": "string", - "enum": ["jest", "none"], - "description": "Test runner to use for unit tests.", - "default": "jest" + "enum": ["none", "jest"], + "description": "Test runner to use for unit tests." }, "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["none", "eslint"] }, "tags": { "type": "string", @@ -64,6 +62,10 @@ "type": "string", "description": "The name of the e2e project.", "x-prompt": "What is the name of the e2e project? Leave blank to skip e2e tests" + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["directory", "name", "project"] diff --git a/packages/plugin/src/generators/create-package/utils/normalize-schema.ts b/packages/plugin/src/generators/create-package/utils/normalize-schema.ts index 5ddfe741d2..85f28a3aa1 100644 --- a/packages/plugin/src/generators/create-package/utils/normalize-schema.ts +++ b/packages/plugin/src/generators/create-package/utils/normalize-schema.ts @@ -1,17 +1,35 @@ -import { readProjectConfiguration, Tree } from '@nx/devkit'; +import { readNxJson, Tree } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import type { LinterType } from '@nx/eslint'; +import { + normalizeLinterOption, + normalizeUnitTestRunnerOption, +} from '@nx/js/src/utils/generator-prompts'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { CreatePackageSchema } from '../schema'; export interface NormalizedSchema extends CreatePackageSchema { bundler: 'swc' | 'tsc'; projectName: string; projectRoot: string; + unitTestRunner: 'jest' | 'none'; + linter: LinterType; + useProjectJson: boolean; + addPlugin: boolean; + isTsSolutionSetup: boolean; } export async function normalizeSchema( host: Tree, schema: CreatePackageSchema ): Promise { + const linter = await normalizeLinterOption(host, schema.linter); + const unitTestRunner = await normalizeUnitTestRunnerOption( + host, + schema.unitTestRunner, + ['jest'] + ); + if (!schema.directory) { throw new Error( `Please provide the --directory option. It should be the directory containing the project '${schema.project}'.` @@ -27,11 +45,25 @@ export async function normalizeSchema( directory: schema.directory, }); + const isTsSolutionSetup = isUsingTsSolutionSetup(host); + const nxJson = readNxJson(host); + const addPlugin = + schema.addPlugin ?? + (isTsSolutionSetup && + process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false); + return { ...schema, bundler: schema.compiler ?? 'tsc', projectName, projectRoot, name: projectNames.projectSimpleName, + linter, + unitTestRunner, + // We default to generate a project.json file if the new setup is not being used + useProjectJson: schema.useProjectJson ?? !isTsSolutionSetup, + addPlugin, + isTsSolutionSetup, }; } diff --git a/packages/plugin/src/generators/e2e-project/e2e.ts b/packages/plugin/src/generators/e2e-project/e2e.ts index 43fc8f71e7..fa948ff94a 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.ts @@ -1,12 +1,8 @@ -import type { Tree } from '@nx/devkit'; import { addProjectConfiguration, - extractLayoutDirectory, formatFiles, generateFiles, - GeneratorCallback, getPackageManagerCommand, - getWorkspaceLayout, joinPathFragments, names, offsetFromRoot, @@ -14,15 +10,29 @@ import { readNxJson, readProjectConfiguration, runTasksInSerial, + updateJson, updateProjectConfiguration, + writeJson, + type GeneratorCallback, + type ProjectConfiguration, + type Tree, } from '@nx/devkit'; -import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { + determineProjectNameAndRootOptions, + resolveImportPath, +} from '@nx/devkit/src/generators/project-name-and-root-utils'; +import { LinterType, lintProjectGenerator } from '@nx/eslint'; import { addPropertyToJestConfig, configurationGenerator } from '@nx/jest'; import { getRelativePathToRootTsConfig } from '@nx/js'; import { setupVerdaccio } from '@nx/js/src/generators/setup-verdaccio/generator'; import { addLocalRegistryScripts } from '@nx/js/src/utils/add-local-registry-scripts'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; -import { Linter, LinterType, lintProjectGenerator } from '@nx/eslint'; +import { normalizeLinterOption } from '@nx/js/src/utils/generator-prompts'; +import { + getProjectPackageManagerWorkspaceState, + getProjectPackageManagerWorkspaceStateWarningTask, +} from '@nx/js/src/utils/package-manager-workspaces'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import type { PackageJson } from 'nx/src/utils/package-json'; import { join } from 'path'; import type { Schema } from './schema'; @@ -30,21 +40,25 @@ interface NormalizedSchema extends Schema { projectRoot: string; projectName: string; pluginPropertyName: string; - linter: Linter | LinterType; + linter: LinterType; + useProjectJson: boolean; + addPlugin: boolean; + isTsSolutionSetup: boolean; } async function normalizeOptions( host: Tree, options: Schema ): Promise { + const linter = await normalizeLinterOption(host, options.linter); + const projectName = options.rootProject ? 'e2e' : `${options.pluginName}-e2e`; const nxJson = readNxJson(host); const addPlugin = - process.env.NX_ADD_PLUGINS !== 'false' && - nxJson.useInferencePlugins !== false; - - options.addPlugin ??= addPlugin; + options.addPlugin ?? + (process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false); let projectRoot: string; const projectNameAndRootOptions = await determineProjectNameAndRootOptions( @@ -61,13 +75,17 @@ async function normalizeOptions( projectRoot = projectNameAndRootOptions.projectRoot; const pluginPropertyName = names(options.pluginName).propertyName; + const isTsSolutionSetup = isUsingTsSolutionSetup(host); return { ...options, projectName, - linter: options.linter ?? Linter.EsLint, + linter, pluginPropertyName, projectRoot, + addPlugin, + useProjectJson: options.useProjectJson ?? !isTsSolutionSetup, + isTsSolutionSetup, }; } @@ -99,13 +117,29 @@ function addFiles(host: Tree, options: NormalizedSchema) { } async function addJest(host: Tree, options: NormalizedSchema) { - addProjectConfiguration(host, options.projectName, { + const projectConfiguration: ProjectConfiguration = { + name: options.projectName, root: options.projectRoot, projectType: 'application', sourceRoot: `${options.projectRoot}/src`, - targets: {}, implicitDependencies: [options.pluginName], - }); + }; + + if (options.isTsSolutionSetup) { + writeJson( + host, + joinPathFragments(options.projectRoot, 'package.json'), + { + name: resolveImportPath(host, options.projectName, options.projectRoot), + version: '0.0.1', + private: true, + } + ); + updateProjectConfiguration(host, options.projectName, projectConfiguration); + } else { + projectConfiguration.targets = {}; + addProjectConfiguration(host, options.projectName, projectConfiguration); + } const jestTask = await configurationGenerator(host, { project: options.projectName, @@ -134,6 +168,7 @@ async function addJest(host: Tree, options: NormalizedSchema) { ); const project = readProjectConfiguration(host, options.projectName); + project.targets ??= {}; const e2eTarget = project.targets.e2e; project.targets.e2e = { @@ -169,16 +204,24 @@ async function addLintingToApplication( return lintTask; } +function updatePluginPackageJson(tree: Tree, options: NormalizedSchema) { + const { root } = readProjectConfiguration(tree, options.pluginName); + updateJson(tree, joinPathFragments(root, 'package.json'), (json) => { + // to publish the plugin, we need to remove the private flag + delete json.private; + return json; + }); +} + export async function e2eProjectGenerator(host: Tree, schema: Schema) { return await e2eProjectGeneratorInternal(host, { addPlugin: false, + useProjectJson: true, ...schema, }); } export async function e2eProjectGeneratorInternal(host: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(host, 'plugin', 'e2e-project'); - const tasks: GeneratorCallback[] = []; validatePlugin(host, schema.pluginName); @@ -190,8 +233,9 @@ export async function e2eProjectGeneratorInternal(host: Tree, schema: Schema) { }) ); tasks.push(await addJest(host, options)); + updatePluginPackageJson(host, options); - if (options.linter !== Linter.None) { + if (options.linter !== 'none') { tasks.push( await addLintingToApplication(host, { ...options, @@ -199,10 +243,37 @@ export async function e2eProjectGeneratorInternal(host: Tree, schema: Schema) { ); } + if (options.isTsSolutionSetup && !options.rootProject) { + // update root tsconfig.json references with the new lib tsconfig + updateJson(host, 'tsconfig.json', (json) => { + json.references ??= []; + json.references.push({ + path: options.projectRoot.startsWith('./') + ? options.projectRoot + : './' + options.projectRoot, + }); + return json; + }); + } + if (!options.skipFormat) { await formatFiles(host); } + if (options.isTsSolutionSetup && !options.skipWorkspacesWarning) { + const projectPackageManagerWorkspaceState = + getProjectPackageManagerWorkspaceState(host, options.projectRoot); + + if (projectPackageManagerWorkspaceState !== 'included') { + tasks.push( + getProjectPackageManagerWorkspaceStateWarningTask( + projectPackageManagerWorkspaceState, + host.root + ) + ); + } + } + return runTasksInSerial(...tasks); } diff --git a/packages/plugin/src/generators/e2e-project/schema.d.ts b/packages/plugin/src/generators/e2e-project/schema.d.ts index 14d72bef28..6e0e4c7360 100644 --- a/packages/plugin/src/generators/e2e-project/schema.d.ts +++ b/packages/plugin/src/generators/e2e-project/schema.d.ts @@ -9,5 +9,7 @@ export interface Schema { linter?: Linter | LinterType; skipFormat?: boolean; rootProject?: boolean; + useProjectJson?: boolean; addPlugin?: boolean; + skipWorkspacesWarning?: boolean; } diff --git a/packages/plugin/src/generators/e2e-project/schema.json b/packages/plugin/src/generators/e2e-project/schema.json index 60b91cdbed..77c4b5ba09 100644 --- a/packages/plugin/src/generators/e2e-project/schema.json +++ b/packages/plugin/src/generators/e2e-project/schema.json @@ -33,8 +33,7 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint", "none"], - "default": "eslint" + "enum": ["none", "eslint"] }, "minimal": { "type": "boolean", @@ -46,6 +45,10 @@ "type": "boolean", "default": false, "x-priority": "internal" + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["pluginName", "npmPackageName"] diff --git a/packages/plugin/src/generators/executor/executor.spec.ts b/packages/plugin/src/generators/executor/executor.spec.ts index 93d60122fe..b08c1327c3 100644 --- a/packages/plugin/src/generators/executor/executor.spec.ts +++ b/packages/plugin/src/generators/executor/executor.spec.ts @@ -28,7 +28,7 @@ describe('NxPlugin Executor Generator', () => { it('should generate files', async () => { await executorGenerator(tree, { name: 'my-executor', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', unitTestRunner: 'jest', includeHasher: false, }); @@ -52,7 +52,7 @@ describe('NxPlugin Executor Generator', () => { await executorGenerator(tree, { name: 'my-executor', unitTestRunner: 'jest', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', includeHasher: false, }); @@ -73,7 +73,7 @@ describe('NxPlugin Executor Generator', () => { it('should update executors.json', async () => { await executorGenerator(tree, { name: 'my-executor', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', unitTestRunner: 'jest', includeHasher: false, }); @@ -94,7 +94,7 @@ describe('NxPlugin Executor Generator', () => { it('should generate custom description', async () => { await executorGenerator(tree, { name: 'my-executor', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', description: 'my-executor custom description', unitTestRunner: 'jest', includeHasher: false, @@ -116,7 +116,7 @@ describe('NxPlugin Executor Generator', () => { await executorGenerator(tree, { name: 'test-executor', - path: 'test-js-lib/src/executors/my-executor', + path: 'test-js-lib/src/executors/my-executor/executor', unitTestRunner: 'jest', includeHasher: false, }); @@ -127,12 +127,56 @@ describe('NxPlugin Executor Generator', () => { ); }); + it('should support custom executor file name', async () => { + await executorGenerator(tree, { + name: 'my-executor', + path: 'my-plugin/src/executors/my-executor/my-custom-executor', + unitTestRunner: 'jest', + includeHasher: true, + }); + + expect( + tree.exists('my-plugin/src/executors/my-executor/schema.d.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/schema.json') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/my-custom-executor.ts') + ).toBeTruthy(); + expect( + tree.exists( + 'my-plugin/src/executors/my-executor/my-custom-executor.spec.ts' + ) + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/hasher.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/executors/my-executor/hasher.spec.ts') + ).toBeTruthy(); + expect(tree.read('my-plugin/executors.json', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "executors": { + "my-executor": { + "implementation": "./src/executors/my-executor/my-custom-executor", + "schema": "./src/executors/my-executor/schema.json", + "description": "my-executor executor", + "hasher": "./src/executors/my-executor/hasher" + } + } + } + " + `); + }); + describe('--unitTestRunner', () => { describe('none', () => { it('should not generate unit test files', async () => { await executorGenerator(tree, { name: 'my-executor', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', unitTestRunner: 'none', includeHasher: false, }); @@ -151,7 +195,7 @@ describe('NxPlugin Executor Generator', () => { it('should generate hasher files', async () => { await executorGenerator(tree, { name: 'my-executor', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', unitTestRunner: 'jest', includeHasher: true, }); @@ -180,7 +224,7 @@ describe('NxPlugin Executor Generator', () => { it('should update executors.json', async () => { await executorGenerator(tree, { name: 'my-executor', - path: 'my-plugin/src/executors/my-executor', + path: 'my-plugin/src/executors/my-executor/executor', unitTestRunner: 'jest', includeHasher: true, }); diff --git a/packages/plugin/src/generators/executor/executor.ts b/packages/plugin/src/generators/executor/executor.ts index 76358c6c6b..92a61691a5 100644 --- a/packages/plugin/src/generators/executor/executor.ts +++ b/packages/plugin/src/generators/executor/executor.ts @@ -1,49 +1,54 @@ import { - readProjectConfiguration, - names, - generateFiles, - updateJson, - joinPathFragments, - writeJson, - readJson, - ExecutorsJson, formatFiles, + generateFiles, + joinPathFragments, + names, + readJson, + readProjectConfiguration, + updateJson, + writeJson, + type ExecutorsJson, + type Tree, } from '@nx/devkit'; -import type { Tree } from '@nx/devkit'; -import type { Schema } from './schema'; -import * as path from 'path'; -import { PackageJson } from 'nx/src/utils/package-json'; -import pluginLintCheckGenerator from '../lint-checks/generator'; -import { nxVersion } from '../../utils/versions'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; -import { relative } from 'path'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { PackageJson } from 'nx/src/utils/package-json'; +import { join } from 'path'; +import { getArtifactMetadataDirectory } from '../../utils/paths'; +import { nxVersion } from '../../utils/versions'; +import pluginLintCheckGenerator from '../lint-checks/generator'; +import type { Schema } from './schema'; interface NormalizedSchema extends Schema { className: string; propertyName: string; projectRoot: string; - filePath: string; + projectSourceRoot: string; + fileName: string; directory: string; project: string; + isTsSolutionSetup: boolean; } function addFiles(host: Tree, options: NormalizedSchema) { - generateFiles(host, path.join(__dirname, './files/executor'), options.path, { + generateFiles(host, join(__dirname, './files/executor'), options.directory, { ...options, }); if (options.unitTestRunner === 'none') { - host.delete(joinPathFragments(options.path, `executor.spec.ts`)); + host.delete( + joinPathFragments(options.directory, `${options.fileName}.spec.ts`) + ); } } function addHasherFiles(host: Tree, options: NormalizedSchema) { - generateFiles(host, path.join(__dirname, './files/hasher'), options.path, { + generateFiles(host, join(__dirname, './files/hasher'), options.directory, { ...options, }); if (options.unitTestRunner === 'none') { - host.delete(joinPathFragments(options.path, 'hasher.spec.ts')); + host.delete(joinPathFragments(options.directory, 'hasher.spec.ts')); } } @@ -96,6 +101,19 @@ async function updateExecutorJson(host: Tree, options: NormalizedSchema) { options.project, options.skipLintChecks ); + + if (options.isTsSolutionSetup) { + updateJson( + host, + joinPathFragments(options.projectRoot, 'package.json'), + (json) => { + const filesSet = new Set(json.files ?? ['dist', '!**/*.tsbuildinfo']); + filesSet.add('executors.json'); + json.files = [...filesSet]; + return json; + } + ); + } } // add dependencies updateJson( @@ -113,22 +131,20 @@ async function updateExecutorJson(host: Tree, options: NormalizedSchema) { return updateJson(host, executorsPath, (json) => { let executors = json.executors ?? json.builders; executors ||= {}; + + const dir = getArtifactMetadataDirectory( + host, + options.project, + options.directory, + options.isTsSolutionSetup + ); executors[options.name] = { - implementation: `./${joinPathFragments( - relative(options.projectRoot, options.path), - 'executor' - )}`, - schema: `./${joinPathFragments( - relative(options.projectRoot, options.path), - 'schema.json' - )}`, + implementation: `${dir}/${options.fileName}`, + schema: `${dir}/schema.json`, description: options.description, }; if (options.includeHasher) { - executors[options.name].hasher = `./${joinPathFragments( - relative(options.projectRoot, options.path), - 'hasher' - )}`; + executors[options.name].hasher = `${dir}/hasher`; } json.executors = executors; @@ -140,33 +156,40 @@ async function normalizeOptions( tree: Tree, options: Schema ): Promise { - const { project, artifactName, filePath, directory } = - await determineArtifactNameAndDirectoryOptions(tree, { - name: options.name, - path: options.path, - fileName: 'executor', - }); + const { + artifactName: name, + directory, + fileName, + project, + } = await determineArtifactNameAndDirectoryOptions(tree, { + path: options.path, + name: options.name, + }); - const { className, propertyName } = names(artifactName); + const { className, propertyName } = names(name); - const { root: projectRoot } = readProjectConfiguration(tree, project); + const { root: projectRoot, sourceRoot: projectSourceRoot } = + readProjectConfiguration(tree, project); let description: string; if (options.description) { description = options.description; } else { - description = `${options.name} executor`; + description = `${name} executor`; } return { ...options, - filePath, + fileName, project, directory, + name, className, propertyName, description, projectRoot, + projectSourceRoot, + isTsSolutionSetup: isUsingTsSolutionSetup(tree), }; } diff --git a/packages/plugin/src/generators/executor/files/executor/executor.spec.ts.template b/packages/plugin/src/generators/executor/files/executor/__fileName__.spec.ts.template similarity index 93% rename from packages/plugin/src/generators/executor/files/executor/executor.spec.ts.template rename to packages/plugin/src/generators/executor/files/executor/__fileName__.spec.ts.template index 3e44f6c74b..c540569e00 100644 --- a/packages/plugin/src/generators/executor/files/executor/executor.spec.ts.template +++ b/packages/plugin/src/generators/executor/files/executor/__fileName__.spec.ts.template @@ -1,7 +1,7 @@ import { ExecutorContext } from '@nx/devkit'; import { <%= className %>ExecutorSchema } from './schema'; -import executor from './executor'; +import executor from './<%= fileName %>'; const options: <%= className %>ExecutorSchema = {}; const context: ExecutorContext = { diff --git a/packages/plugin/src/generators/executor/files/executor/executor.ts.template b/packages/plugin/src/generators/executor/files/executor/__fileName__.ts.template similarity index 100% rename from packages/plugin/src/generators/executor/files/executor/executor.ts.template rename to packages/plugin/src/generators/executor/files/executor/__fileName__.ts.template diff --git a/packages/plugin/src/generators/generator/files/generator/generator.spec.ts.template b/packages/plugin/src/generators/generator/files/generator/__fileName__.spec.ts.template similarity index 90% rename from packages/plugin/src/generators/generator/files/generator/generator.spec.ts.template rename to packages/plugin/src/generators/generator/files/generator/__fileName__.spec.ts.template index d4b8c36a7f..f8f55ddefc 100644 --- a/packages/plugin/src/generators/generator/files/generator/generator.spec.ts.template +++ b/packages/plugin/src/generators/generator/files/generator/__fileName__.spec.ts.template @@ -1,7 +1,7 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Tree, readProjectConfiguration } from '@nx/devkit'; -import { <%= generatorFnName %> } from './generator'; +import { <%= generatorFnName %> } from './<%= fileName %>'; import { <%= schemaInterfaceName %> } from './schema'; describe('<%= name %> generator', () => { diff --git a/packages/plugin/src/generators/generator/files/generator/generator.ts.template b/packages/plugin/src/generators/generator/files/generator/__fileName__.ts.template similarity index 100% rename from packages/plugin/src/generators/generator/files/generator/generator.ts.template rename to packages/plugin/src/generators/generator/files/generator/__fileName__.ts.template diff --git a/packages/plugin/src/generators/generator/generator.spec.ts b/packages/plugin/src/generators/generator/generator.spec.ts index bd67b9fd0e..619249c8fa 100644 --- a/packages/plugin/src/generators/generator/generator.spec.ts +++ b/packages/plugin/src/generators/generator/generator.spec.ts @@ -32,7 +32,7 @@ describe('NxPlugin Generator Generator', () => { it('should generate files', async () => { await generatorGenerator(tree, { name: 'my-generator', - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', unitTestRunner: 'jest', }); @@ -54,7 +54,7 @@ describe('NxPlugin Generator Generator', () => { setCwd('my-plugin/src/nx-integrations/generators/my-generator'); await generatorGenerator(tree, { name: 'my-generator', - path: 'my-plugin/src/nx-integrations/generators/my-generator', + path: 'my-plugin/src/nx-integrations/generators/my-generator/generator', unitTestRunner: 'jest', }); @@ -82,7 +82,7 @@ describe('NxPlugin Generator Generator', () => { it('should generate files for derived', async () => { await generatorGenerator(tree, { - path: `${projectName}/src/generators/my-generator`, + path: `${projectName}/src/generators/my-generator/generator`, name: 'my-generator', unitTestRunner: 'jest', }); @@ -104,7 +104,7 @@ describe('NxPlugin Generator Generator', () => { it('should update generators.json', async () => { await generatorGenerator(tree, { name: 'my-generator', - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', unitTestRunner: 'jest', }); @@ -123,7 +123,7 @@ describe('NxPlugin Generator Generator', () => { it('should update generators.json for derived', async () => { await generatorGenerator(tree, { - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', name: 'my-generator', unitTestRunner: 'jest', }); @@ -144,12 +144,12 @@ describe('NxPlugin Generator Generator', () => { it('should throw if recreating an existing generator', async () => { await generatorGenerator(tree, { name: 'my-generator', - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', unitTestRunner: 'jest', }); expect( generatorGenerator(tree, { - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', name: 'my-generator', unitTestRunner: 'jest', }) @@ -162,7 +162,7 @@ describe('NxPlugin Generator Generator', () => { await generatorGenerator(tree, { name: generatorName, - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', unitTestRunner: 'jest', description: 'my-generator description', }); @@ -192,7 +192,7 @@ describe('NxPlugin Generator Generator', () => { const libConfig = readProjectConfiguration(tree, 'test-js-lib'); await generatorGenerator(tree, { name: 'test-generator', - path: 'test-js-lib/src/generators/test-generator', + path: 'test-js-lib/src/generators/test-generator/generator', unitTestRunner: 'jest', }); @@ -207,7 +207,7 @@ describe('NxPlugin Generator Generator', () => { it('should generate custom description', async () => { await generatorGenerator(tree, { name: 'my-generator', - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', description: 'my-generator custom description', unitTestRunner: 'jest', }); @@ -219,12 +219,50 @@ describe('NxPlugin Generator Generator', () => { ); }); + it('should support custom generator file name', async () => { + await generatorGenerator(tree, { + name: 'my-generator', + path: 'my-plugin/src/generators/my-generator/my-custom-generator', + unitTestRunner: 'jest', + }); + + expect( + tree.exists('my-plugin/src/generators/my-generator/schema.d.ts') + ).toBeTruthy(); + expect( + tree.exists('my-plugin/src/generators/my-generator/schema.json') + ).toBeTruthy(); + expect( + tree.exists( + 'my-plugin/src/generators/my-generator/my-custom-generator.ts' + ) + ).toBeTruthy(); + expect( + tree.exists( + 'my-plugin/src/generators/my-generator/my-custom-generator.spec.ts' + ) + ).toBeTruthy(); + expect(tree.read('my-plugin/generators.json', 'utf-8')) + .toMatchInlineSnapshot(` + "{ + "generators": { + "my-generator": { + "factory": "./src/generators/my-generator/my-custom-generator", + "schema": "./src/generators/my-generator/schema.json", + "description": "my-generator generator" + } + } + } + " + `); + }); + describe('--unitTestRunner', () => { describe('none', () => { it('should not generate files', async () => { await generatorGenerator(tree, { name: 'my-generator', - path: 'my-plugin/src/generators/my-generator', + path: 'my-plugin/src/generators/my-generator/generator', unitTestRunner: 'none', }); @@ -241,7 +279,7 @@ describe('NxPlugin Generator Generator', () => { describe('preset generator', () => { it('should default to standalone layout: true', async () => { await generatorGenerator(tree, { - path: 'my-plugin/src/generators/preset', + path: 'my-plugin/src/generators/preset/generator', name: 'preset', unitTestRunner: 'none', }); diff --git a/packages/plugin/src/generators/generator/generator.ts b/packages/plugin/src/generators/generator/generator.ts index 92c3883cd4..45a9469a98 100644 --- a/packages/plugin/src/generators/generator/generator.ts +++ b/packages/plugin/src/generators/generator/generator.ts @@ -1,22 +1,24 @@ import { formatFiles, + generateFiles, GeneratorsJson, joinPathFragments, - Tree, - writeJson, - generateFiles, names, readJson, readProjectConfiguration, + Tree, updateJson, + writeJson, } from '@nx/devkit'; +import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { join } from 'node:path'; import { PackageJson } from 'nx/src/utils/package-json'; import { hasGenerator } from '../../utils/has-generator'; +import { getArtifactMetadataDirectory } from '../../utils/paths'; +import { nxVersion } from '../../utils/versions'; import pluginLintCheckGenerator from '../lint-checks/generator'; import type { Schema } from './schema'; -import { nxVersion } from '../../utils/versions'; -import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; -import { join, relative } from 'path'; type NormalizedSchema = Schema & { directory: string; @@ -26,20 +28,24 @@ type NormalizedSchema = Schema & { projectRoot: string; projectSourceRoot: string; project: string; + isTsSolutionSetup: boolean; }; async function normalizeOptions( tree: Tree, options: Schema ): Promise { - const { project, fileName, artifactName, filePath, directory } = - await determineArtifactNameAndDirectoryOptions(tree, { - name: options.name, - path: options.path, - fileName: 'generator', - }); + const { + artifactName: name, + directory, + fileName, + project, + } = await determineArtifactNameAndDirectoryOptions(tree, { + path: options.path, + name: options.name, + }); - const { className, propertyName } = names(artifactName); + const { className, propertyName } = names(name); const { root: projectRoot, sourceRoot: projectSourceRoot } = readProjectConfiguration(tree, project); @@ -48,37 +54,39 @@ async function normalizeOptions( if (options.description) { description = options.description; } else { - description = `${options.name} generator`; + description = `${name} generator`; } return { ...options, directory, project, + name, fileName, className, propertyName, description, projectRoot, projectSourceRoot, + isTsSolutionSetup: isUsingTsSolutionSetup(tree), }; } function addFiles(host: Tree, options: NormalizedSchema) { - const indexPath = join(options.path, 'files/src/index.ts.template'); + const indexPath = join(options.directory, 'files/src/index.ts.template'); if (!host.exists(indexPath)) { host.write(indexPath, 'const variable = "<%= name %>";'); } - generateFiles(host, join(__dirname, './files/generator'), options.path, { + generateFiles(host, join(__dirname, './files/generator'), options.directory, { ...options, generatorFnName: `${options.propertyName}Generator`, schemaInterfaceName: `${options.className}GeneratorSchema`, }); if (options.unitTestRunner === 'none') { - host.delete(join(options.path, `generator.spec.ts`)); + host.delete(join(options.directory, `${options.fileName}.spec.ts`)); } } @@ -134,6 +142,19 @@ async function updateGeneratorJson(host: Tree, options: NormalizedSchema) { options.skipLintChecks, options.skipFormat ); + + if (options.isTsSolutionSetup) { + updateJson( + host, + joinPathFragments(options.projectRoot, 'package.json'), + (json) => { + const filesSet = new Set(json.files ?? ['dist', '!**/*.tsbuildinfo']); + filesSet.add('generators.json'); + json.files = [...filesSet]; + return json; + } + ); + } } // add dependencies updateJson( @@ -151,15 +172,16 @@ async function updateGeneratorJson(host: Tree, options: NormalizedSchema) { updateJson(host, generatorsPath, (json) => { let generators = json.generators ?? json.schematics; generators = generators || {}; + + const dir = getArtifactMetadataDirectory( + host, + options.project, + options.directory, + options.isTsSolutionSetup + ); generators[options.name] = { - factory: `./${joinPathFragments( - relative(options.projectRoot, options.path), - 'generator' - )}`, - schema: `./${joinPathFragments( - relative(options.projectRoot, options.path), - 'schema.json' - )}`, + factory: `${dir}/${options.fileName}`, + schema: `${dir}/schema.json`, description: options.description, }; // @todo(v17): Remove this, prop is defunct. diff --git a/packages/plugin/src/generators/migration/files/migration/__name__.spec.ts__tmpl__ b/packages/plugin/src/generators/migration/files/migration/__fileName__.spec.ts__tmpl__ similarity index 100% rename from packages/plugin/src/generators/migration/files/migration/__name__.spec.ts__tmpl__ rename to packages/plugin/src/generators/migration/files/migration/__fileName__.spec.ts__tmpl__ diff --git a/packages/plugin/src/generators/migration/files/migration/__name__.ts__tmpl__ b/packages/plugin/src/generators/migration/files/migration/__fileName__.ts__tmpl__ similarity index 100% rename from packages/plugin/src/generators/migration/files/migration/__name__.ts__tmpl__ rename to packages/plugin/src/generators/migration/files/migration/__fileName__.ts__tmpl__ diff --git a/packages/plugin/src/generators/migration/migration.spec.ts b/packages/plugin/src/generators/migration/migration.spec.ts index 3909ac86f8..53ad5d0229 100644 --- a/packages/plugin/src/generators/migration/migration.spec.ts +++ b/packages/plugin/src/generators/migration/migration.spec.ts @@ -27,7 +27,7 @@ describe('NxPlugin migration generator', () => { it('should update the workspace.json file', async () => { await migrationGenerator(tree, { - path: `packages/my-plugin/${projectName}`, + path: `packages/my-plugin/${projectName}/update-1.0.0`, packageVersion: '1.0.0', }); @@ -43,7 +43,7 @@ describe('NxPlugin migration generator', () => { it('should generate files', async () => { await migrationGenerator(tree, { name: 'my-migration', - path: 'packages/my-plugin/migrations/1.0.0', + path: 'packages/my-plugin/migrations/1.0.0/my-migration', packageVersion: '1.0.0', }); @@ -71,7 +71,7 @@ describe('NxPlugin migration generator', () => { it('should generate files with default name', async () => { await migrationGenerator(tree, { description: 'my-migration description', - path: 'packages/my-plugin/src/migrations/update-1.0.0', + path: 'packages/my-plugin/src/migrations/update-1.0.0/update-1.0.0', packageVersion: '1.0.0', }); @@ -91,7 +91,7 @@ describe('NxPlugin migration generator', () => { it('should generate files with default description', async () => { await migrationGenerator(tree, { name: 'my-migration', - path: 'packages/my-plugin/src/migrations/update-1.0.0', + path: 'packages/my-plugin/src/migrations/update-1.0.0/update-1.0.0', packageVersion: '1.0.0', }); @@ -105,7 +105,7 @@ describe('NxPlugin migration generator', () => { it('should generate files with package.json updates', async () => { await migrationGenerator(tree, { name: 'my-migration', - path: 'packages/my-plugin/src/migrations/update-1.0.0', + path: 'packages/my-plugin/src/migrations/update-1.0.0/update-1.0.0', packageVersion: '1.0.0', packageJsonUpdates: true, }); diff --git a/packages/plugin/src/generators/migration/migration.ts b/packages/plugin/src/generators/migration/migration.ts index 49d8a05d4b..1429fa6e49 100644 --- a/packages/plugin/src/generators/migration/migration.ts +++ b/packages/plugin/src/generators/migration/migration.ts @@ -1,47 +1,45 @@ -import type { Tree } from '@nx/devkit'; import { formatFiles, generateFiles, joinPathFragments, - names, readJson, readProjectConfiguration, updateJson, updateProjectConfiguration, writeJson, + type Tree, } from '@nx/devkit'; -import type { Schema } from './schema'; -import * as path from 'path'; -import { relative } from 'path'; -import { addMigrationJsonChecks } from '../lint-checks/generator'; -import { PackageJson, readNxMigrateConfig } from 'nx/src/utils/package-json'; -import { nxVersion } from '../../utils/versions'; import { determineArtifactNameAndDirectoryOptions } from '@nx/devkit/src/generators/artifact-name-and-directory-utils'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { join } from 'node:path'; +import { PackageJson, readNxMigrateConfig } from 'nx/src/utils/package-json'; +import { getArtifactMetadataDirectory } from '../../utils/paths'; +import { nxVersion } from '../../utils/versions'; +import { addMigrationJsonChecks } from '../lint-checks/generator'; +import type { Schema } from './schema'; interface NormalizedSchema extends Schema { directory: string; + fileName: string; projectRoot: string; projectSourceRoot: string; project: string; + isTsSolutionSetup: boolean; } async function normalizeOptions( tree: Tree, options: Schema ): Promise { - let name: string; - if (options.name) { - name = names(options.name).fileName; - } else { - name = names(`update-${options.packageVersion}`).fileName; - } - - const { project, fileName, artifactName, filePath, directory } = - await determineArtifactNameAndDirectoryOptions(tree, { - name: name, - path: options.path, - fileName: name, - }); + const { + artifactName: name, + directory, + fileName, + project, + } = await determineArtifactNameAndDirectoryOptions(tree, { + path: options.path, + name: options.name, + }); const { root: projectRoot, sourceRoot: projectSourceRoot } = readProjectConfiguration(tree, project); @@ -49,24 +47,23 @@ async function normalizeOptions( const description: string = options.description ?? `Migration for v${options.packageVersion}`; - // const { root: projectRoot, sourceRoot: projectSourceRoot } = - // readProjectConfiguration(host, options.project); - const normalized: NormalizedSchema = { ...options, directory, + fileName, project, - name: artifactName, + name, description, projectRoot, projectSourceRoot, + isTsSolutionSetup: isUsingTsSolutionSetup(tree), }; return normalized; } function addFiles(host: Tree, options: NormalizedSchema) { - generateFiles(host, path.join(__dirname, 'files/migration'), options.path, { + generateFiles(host, join(__dirname, 'files/migration'), options.directory, { ...options, tmpl: '', }); @@ -88,13 +85,17 @@ function updateMigrationsJson(host: Tree, options: NormalizedSchema) { : {}; const generators = migrations.generators ?? {}; + + const dir = getArtifactMetadataDirectory( + host, + options.project, + options.directory, + options.isTsSolutionSetup + ); generators[options.name] = { version: options.packageVersion, description: options.description, - implementation: `./${joinPathFragments( - relative(options.projectRoot, options.path), - options.name - )}`, + implementation: `${dir}/${options.fileName}`, }; migrations.generators = generators; @@ -115,20 +116,30 @@ function updateMigrationsJson(host: Tree, options: NormalizedSchema) { function updatePackageJson(host: Tree, options: NormalizedSchema) { updateJson( host, - path.join(options.projectRoot, 'package.json'), + join(options.projectRoot, 'package.json'), (json) => { + const addFile = (file: string) => { + if (options.isTsSolutionSetup) { + const filesSet = new Set(json.files ?? ['dist', '!**/*.tsbuildinfo']); + filesSet.add(file.replace(/^\.\//, '')); + json.files = [...filesSet]; + } + }; + const migrationKey = json['ng-update'] ? 'ng-update' : 'nx-migrations'; - const preexistingValue = json[migrationKey]; - if (typeof preexistingValue === 'string') { + if (typeof json[migrationKey] === 'string') { + addFile(json[migrationKey]); return json; } else if (!json[migrationKey]) { json[migrationKey] = { migrations: './migrations.json', }; - } else if (preexistingValue.migrations) { - preexistingValue.migrations = './migrations.json'; + } else if (json[migrationKey].migrations) { + json[migrationKey].migrations = './migrations.json'; } + addFile(json[migrationKey].migrations); + // add dependencies json.dependencies = { '@nx/devkit': nxVersion, @@ -141,6 +152,10 @@ function updatePackageJson(host: Tree, options: NormalizedSchema) { } function updateProjectConfig(host: Tree, options: NormalizedSchema) { + if (options.isTsSolutionSetup) { + return; + } + const project = readProjectConfiguration(host, options.project); const assets = project.targets.build?.options?.assets; @@ -164,10 +179,9 @@ export async function migrationGenerator(host: Tree, schema: Schema) { const options = await normalizeOptions(host, schema); addFiles(host, options); + updatePackageJson(host, options); updateMigrationsJson(host, options); updateProjectConfig(host, options); - updateMigrationsJson(host, options); - updatePackageJson(host, options); if (!host.exists('migrations.json')) { const packageJsonPath = joinPathFragments( diff --git a/packages/plugin/src/generators/plugin/plugin.ts b/packages/plugin/src/generators/plugin/plugin.ts index 06deba1cbe..5d7e048301 100644 --- a/packages/plugin/src/generators/plugin/plugin.ts +++ b/packages/plugin/src/generators/plugin/plugin.ts @@ -10,18 +10,22 @@ import { Tree, updateProjectConfiguration, } from '@nx/devkit'; -import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; -import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { Linter } from '@nx/eslint'; +import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; +import { + getProjectPackageManagerWorkspaceState, + getProjectPackageManagerWorkspaceStateWarningTask, +} from '@nx/js/src/utils/package-manager-workspaces'; +import { + addSwcDependencies, + addSwcRegisterDependencies, +} from '@nx/js/src/utils/swc/add-swc-dependencies'; +import { addTsLibDependencies } from '@nx/js/src/utils/typescript/add-tslib-dependencies'; import * as path from 'path'; import { e2eProjectGenerator } from '../e2e-project/e2e'; import pluginLintCheckGenerator from '../lint-checks/generator'; -import { NormalizedSchema, normalizeOptions } from './utils/normalize-schema'; -import { addTsLibDependencies } from '@nx/js/src/utils/typescript/add-tslib-dependencies'; -import { addSwcRegisterDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; - import type { Schema } from './schema'; +import { NormalizedSchema, normalizeOptions } from './utils/normalize-schema'; const nxVersion = require('../../../package.json').version; @@ -43,40 +47,44 @@ function updatePluginConfig(host: Tree, options: NormalizedSchema) { const project = readProjectConfiguration(host, options.name); if (project.targets.build) { - project.targets.build.options.assets ??= []; + if (options.isTsSolutionSetup && options.bundler === 'tsc') { + project.targets.build.options.rootDir = project.sourceRoot; + project.targets.build.options.generatePackageJson = false; + } + + project.targets.build.options.assets = [ + ...(project.targets.build.options.assets ?? []), + ]; const root = options.projectRoot === '.' ? '.' : './' + options.projectRoot; - project.targets.build.options.assets = [ - ...project.targets.build.options.assets, - { - input: `${root}/src`, - glob: '**/!(*.ts)', - output: './src', - }, - { - input: `${root}/src`, - glob: '**/*.d.ts', - output: './src', - }, - { - input: root, - glob: 'generators.json', - output: '.', - }, - { - input: root, - glob: 'executors.json', - output: '.', - }, - ]; + + if (options.isTsSolutionSetup) { + project.targets.build.options.assets.push( + { input: `${root}/src`, glob: '**/!(*.ts)', output: '.' }, + { input: `${root}/src`, glob: '**/*.d.ts', output: '.' } + ); + } else { + project.targets.build.options.assets.push( + { input: `${root}/src`, glob: '**/!(*.ts)', output: './src' }, + { input: `${root}/src`, glob: '**/*.d.ts', output: './src' }, + { input: root, glob: 'generators.json', output: '.' }, + { input: root, glob: 'executors.json', output: '.' } + ); + } updateProjectConfiguration(host, options.name, project); } } -export async function pluginGenerator(host: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(host, 'plugin', 'plugin'); +export async function pluginGenerator(tree: Tree, schema: Schema) { + return await pluginGeneratorInternal(tree, { + useProjectJson: true, + addPlugin: false, + ...schema, + }); +} +export async function pluginGeneratorInternal(host: Tree, schema: Schema) { const options = await normalizeOptions(host, schema); const tasks: GeneratorCallback[] = []; @@ -89,7 +97,13 @@ export async function pluginGenerator(host: Tree, schema: Schema) { bundler: options.bundler, publishable: options.publishable, importPath: options.npmPackageName, + linter: options.linter, + unitTestRunner: options.unitTestRunner, + useProjectJson: options.useProjectJson, + addPlugin: options.addPlugin, skipFormat: true, + skipWorkspacesWarning: true, + useTscExecutor: true, }) ); @@ -131,6 +145,10 @@ export async function pluginGenerator(host: Tree, schema: Schema) { npmPackageName: options.npmPackageName, skipFormat: true, rootProject: options.rootProject, + linter: options.linter, + useProjectJson: options.useProjectJson, + addPlugin: options.addPlugin, + skipWorkspacesWarning: true, }) ); } @@ -143,6 +161,20 @@ export async function pluginGenerator(host: Tree, schema: Schema) { await formatFiles(host); } + if (options.isTsSolutionSetup) { + const projectPackageManagerWorkspaceState = + getProjectPackageManagerWorkspaceState(host, options.projectRoot); + + if (projectPackageManagerWorkspaceState !== 'included') { + tasks.push( + getProjectPackageManagerWorkspaceStateWarningTask( + projectPackageManagerWorkspaceState, + host.root + ) + ); + } + } + return runTasksInSerial(...tasks); } diff --git a/packages/plugin/src/generators/plugin/schema.d.ts b/packages/plugin/src/generators/plugin/schema.d.ts index 3ca1321201..f6e512c866 100644 --- a/packages/plugin/src/generators/plugin/schema.d.ts +++ b/packages/plugin/src/generators/plugin/schema.d.ts @@ -9,10 +9,12 @@ export interface Schema { skipLintChecks?: boolean; // default is false e2eTestRunner?: 'jest' | 'none'; tags?: string; - unitTestRunner: 'jest' | 'none'; - linter: Linter | LinterType; + unitTestRunner?: 'jest' | 'none'; + linter?: Linter | LinterType; setParserOptionsProject?: boolean; - compiler: 'swc' | 'tsc'; + compiler?: 'swc' | 'tsc'; rootProject?: boolean; publishable?: boolean; + useProjectJson?: boolean; + addPlugin?: boolean; } diff --git a/packages/plugin/src/generators/plugin/schema.json b/packages/plugin/src/generators/plugin/schema.json index 62e4954740..34469240b1 100644 --- a/packages/plugin/src/generators/plugin/schema.json +++ b/packages/plugin/src/generators/plugin/schema.json @@ -34,14 +34,14 @@ "linter": { "description": "The tool to use for running lint checks.", "type": "string", - "enum": ["eslint"], - "default": "eslint" + "enum": ["none", "eslint"], + "x-priority": "important" }, "unitTestRunner": { - "type": "string", - "enum": ["jest", "none"], "description": "Test runner to use for unit tests.", - "default": "jest" + "type": "string", + "enum": ["none", "jest"], + "x-priority": "important" }, "tags": { "type": "string", @@ -92,6 +92,10 @@ "type": "boolean", "description": "Generates a boilerplate for publishing the plugin to npm.", "default": false + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["directory"] diff --git a/packages/plugin/src/generators/plugin/utils/normalize-schema.ts b/packages/plugin/src/generators/plugin/utils/normalize-schema.ts index c2453d6222..632ec818a1 100644 --- a/packages/plugin/src/generators/plugin/utils/normalize-schema.ts +++ b/packages/plugin/src/generators/plugin/utils/normalize-schema.ts @@ -1,9 +1,15 @@ -import { Tree, extractLayoutDirectory, getWorkspaceLayout } from '@nx/devkit'; +import { readNxJson, type Tree } from '@nx/devkit'; import { determineProjectNameAndRootOptions, ensureProjectName, } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { Schema } from '../schema'; +import type { LinterType } from '@nx/eslint'; +import { + normalizeLinterOption, + normalizeUnitTestRunnerOption, +} from '@nx/js/src/utils/generator-prompts'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import type { Schema } from '../schema'; export interface NormalizedSchema extends Schema { name: string; @@ -14,12 +20,32 @@ export interface NormalizedSchema extends Schema { npmPackageName: string; bundler: 'swc' | 'tsc'; publishable: boolean; + unitTestRunner: 'jest' | 'none'; + linter: LinterType; + useProjectJson: boolean; + addPlugin: boolean; + isTsSolutionSetup: boolean; } export async function normalizeOptions( host: Tree, options: Schema ): Promise { + const linter = await normalizeLinterOption(host, options.linter); + const unitTestRunner = await normalizeUnitTestRunnerOption( + host, + options.unitTestRunner, + ['jest'] + ); + + const isTsSolutionSetup = isUsingTsSolutionSetup(host); + const nxJson = readNxJson(host); + const addPlugin = + options.addPlugin ?? + (isTsSolutionSetup && + process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false); + await ensureProjectName(host, options, 'application'); const { projectName, @@ -50,5 +76,11 @@ export async function normalizeOptions( parsedTags, npmPackageName, publishable: options.publishable ?? false, + linter, + unitTestRunner, + // We default to generate a project.json file if the new setup is not being used + useProjectJson: options.useProjectJson ?? !isTsSolutionSetup, + addPlugin, + isTsSolutionSetup, }; } diff --git a/packages/plugin/src/generators/preset/generator.ts b/packages/plugin/src/generators/preset/generator.ts index 58f64ac4a4..4491c1dc92 100644 --- a/packages/plugin/src/generators/preset/generator.ts +++ b/packages/plugin/src/generators/preset/generator.ts @@ -1,34 +1,42 @@ import { formatFiles, - GeneratorCallback, names, + readNxJson, runTasksInSerial, - Tree, updateJson, + type GeneratorCallback, + type Tree, } from '@nx/devkit'; -import { Linter } from '@nx/eslint'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; -import { PackageJson } from 'nx/src/utils/package-json'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import type { PackageJson } from 'nx/src/utils/package-json'; +import { createPackageGenerator } from '../create-package/create-package'; import { pluginGenerator } from '../plugin/plugin'; -import { PresetGeneratorSchema } from './schema'; -import createPackageGenerator from '../create-package/create-package'; +import type { + NormalizedPresetGeneratorOptions, + PresetGeneratorSchema, +} from './schema'; -export default async function (tree: Tree, options: PresetGeneratorSchema) { - assertNotUsingTsSolutionSetup(tree, 'plugin', 'preset'); +export async function presetGenerator( + tree: Tree, + rawOptions: PresetGeneratorSchema +) { + return await presetGeneratorInternal(tree, { + addPlugin: false, + useProjectJson: true, + ...rawOptions, + }); +} +export async function presetGeneratorInternal( + tree: Tree, + rawOptions: PresetGeneratorSchema +) { const tasks: GeneratorCallback[] = []; - const pluginProjectName = names( - options.pluginName.includes('/') - ? options.pluginName.split('/')[1] - : options.pluginName - ).fileName; - options.createPackageName = - options.createPackageName === 'false' // for command line in e2e, it is passed as a string - ? undefined - : options.createPackageName; + const options = normalizeOptions(tree, rawOptions); + const pluginTask = await pluginGenerator(tree, { compiler: 'tsc', - linter: Linter.EsLint, + linter: 'eslint', skipFormat: true, unitTestRunner: 'jest', importPath: options.pluginName, @@ -37,9 +45,11 @@ export default async function (tree: Tree, options: PresetGeneratorSchema) { // when creating a CLI package, the plugin will be in the packages folder directory: options.createPackageName && options.createPackageName !== 'false' - ? `packages/${pluginProjectName}` - : pluginProjectName, + ? `packages/${options.pluginName}` + : options.pluginName, rootProject: options.createPackageName ? false : true, + useProjectJson: options.useProjectJson, + addPlugin: options.addPlugin, }); tasks.push(pluginTask); @@ -54,8 +64,10 @@ export default async function (tree: Tree, options: PresetGeneratorSchema) { project: options.pluginName, skipFormat: true, unitTestRunner: 'jest', - linter: Linter.EsLint, + linter: 'eslint', compiler: 'tsc', + useProjectJson: options.useProjectJson, + addPlugin: options.addPlugin, }); tasks.push(cliTask); } @@ -67,9 +79,42 @@ export default async function (tree: Tree, options: PresetGeneratorSchema) { function moveNxPluginToDevDeps(tree: Tree) { updateJson(tree, 'package.json', (json) => { - const nxPluginEntry = json.dependencies['@nx/plugin']; - delete json.dependencies['@nx/plugin']; - json.devDependencies['@nx/plugin'] = nxPluginEntry; + if (json.dependencies['@nx/plugin']) { + const nxPluginEntry = json.dependencies['@nx/plugin']; + delete json.dependencies['@nx/plugin']; + json.devDependencies['@nx/plugin'] = nxPluginEntry; + } return json; }); } + +function normalizeOptions( + tree: Tree, + options: PresetGeneratorSchema +): NormalizedPresetGeneratorOptions { + const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + + const nxJson = readNxJson(tree); + const addPlugin = + options.addPlugin ?? + (isTsSolutionSetup && + process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false); + + return { + ...options, + pluginName: names( + options.pluginName.includes('/') + ? options.pluginName.split('/')[1] + : options.pluginName + ).fileName, + createPackageName: + options.createPackageName === 'false' // for command line in e2e, it is passed as a string + ? undefined + : options.createPackageName, + addPlugin, + useProjectJson: options.useProjectJson ?? !isTsSolutionSetup, + }; +} + +export default presetGenerator; diff --git a/packages/plugin/src/generators/preset/schema.d.ts b/packages/plugin/src/generators/preset/schema.d.ts index b7ed9dfbb5..ab0681b059 100644 --- a/packages/plugin/src/generators/preset/schema.d.ts +++ b/packages/plugin/src/generators/preset/schema.d.ts @@ -1,4 +1,13 @@ export interface PresetGeneratorSchema { pluginName: string; createPackageName?: string; + useProjectJson?: boolean; + addPlugin?: boolean; +} + +export interface NormalizedPresetGeneratorOptions + extends PresetGeneratorSchema { + createPackageName: string; + useProjectJson: boolean; + addPlugin: boolean; } diff --git a/packages/plugin/src/generators/preset/schema.json b/packages/plugin/src/generators/preset/schema.json index e4573011d1..d9e23e727a 100644 --- a/packages/plugin/src/generators/preset/schema.json +++ b/packages/plugin/src/generators/preset/schema.json @@ -24,6 +24,10 @@ "createPackageName": { "type": "string", "description": "Name of package which creates a workspace" + }, + "useProjectJson": { + "type": "boolean", + "description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file." } }, "required": ["pluginName"] diff --git a/packages/plugin/src/utils/paths.ts b/packages/plugin/src/utils/paths.ts new file mode 100644 index 0000000000..3f4bbb31e4 --- /dev/null +++ b/packages/plugin/src/utils/paths.ts @@ -0,0 +1,49 @@ +import { readProjectConfiguration, type Tree } from '@nx/devkit'; +import { dirname, join, relative } from 'node:path/posix'; + +export function getArtifactMetadataDirectory( + tree: Tree, + projectName: string, + sourceDirectory: string, + isTsSolutionSetup: boolean +): string { + const project = readProjectConfiguration(tree, projectName); + + if (!isTsSolutionSetup) { + return `./${relative(project.root, sourceDirectory)}`; + } + + const target = Object.values(project.targets ?? {}).find( + (t) => t.executor === '@nx/js:tsc' || t.executor === '@nx/js:swc' + ); + + // the repo is using the new ts setup where the outputs are contained inside the project + if (target?.executor === '@nx/js:tsc') { + // the @nx/js:tsc executor defaults rootDir to the project root + return `./${join( + 'dist', + relative(target.options.rootDir ?? project.root, sourceDirectory) + )}`; + } + + if (target?.executor === '@nx/js:swc') { + return `./${join( + 'dist', + target.options.stripLeadingPaths + ? relative(dirname(target.options.main), sourceDirectory) + : relative(project.root, sourceDirectory) + )}`; + } + + // We generate the plugin with the executors above, so we shouldn't get here + // unless the user manually changed the build process. In that case, we can't + // reliably determine the output directory because it depends on the build + // tool, so we'll just assume some defaults. + const baseDir = + project.sourceRoot ?? + (tree.exists(join(project.root, 'src')) + ? join(project.root, 'src') + : project.root); + + return `./${join('dist', relative(baseDir, sourceDirectory))}`; +} diff --git a/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__ b/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__ index def52c91f1..9191393c99 100644 --- a/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__ +++ b/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__ @@ -1,7 +1,7 @@ { - "extends": "./tsconfig.json", + "extends": "<%= extendedConfig %>", "compilerOptions": { - "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "outDir": "<%= outDir %>", "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] }, "include": [ diff --git a/packages/vite/src/generators/vitest/vitest-generator.ts b/packages/vite/src/generators/vitest/vitest-generator.ts index 771aa08fd6..1b03dd1e73 100644 --- a/packages/vite/src/generators/vitest/vitest-generator.ts +++ b/packages/vite/src/generators/vitest/vitest-generator.ts @@ -13,21 +13,20 @@ import { Tree, updateJson, } from '@nx/devkit'; +import { initGenerator as jsInitGenerator } from '@nx/js'; +import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { join } from 'path'; +import { ensureDependencies } from '../../utils/ensure-dependencies'; import { addOrChangeTestTarget, createOrEditViteConfig, } from '../../utils/generator-utils'; -import { VitestGeneratorSchema } from './schema'; - -import initGenerator from '../init/init'; import { vitestCoverageIstanbulVersion, vitestCoverageV8Version, } from '../../utils/versions'; - -import { addTsLibDependencies, initGenerator as jsInitGenerator } from '@nx/js'; -import { join } from 'path'; -import { ensureDependencies } from '../../utils/ensure-dependencies'; +import initGenerator from '../init/init'; +import { VitestGeneratorSchema } from './schema'; export function vitestGenerator( tree: Tree, @@ -270,11 +269,17 @@ function createFiles( options: VitestGeneratorSchema, projectRoot: string ) { + const isTsSolutionSetup = isUsingTsSolutionSetup(tree); + const rootOffset = offsetFromRoot(projectRoot); + generateFiles(tree, join(__dirname, 'files'), projectRoot, { tmpl: '', ...options, projectRoot, - offsetFromRoot: offsetFromRoot(projectRoot), + extendedConfig: isTsSolutionSetup + ? `${rootOffset}tsconfig.base.json` + : './tsconfig.json', + outDir: isTsSolutionSetup ? `./out-tsc/jest` : `${rootOffset}dist/out-tsc`, }); } diff --git a/packages/workspace/src/generators/new/files-integrated-repo/__dot__gitignore b/packages/workspace/src/generators/new/files-integrated-repo/__dot__gitignore index 4f4d87b1ce..ccbf37187c 100644 --- a/packages/workspace/src/generators/new/files-integrated-repo/__dot__gitignore +++ b/packages/workspace/src/generators/new/files-integrated-repo/__dot__gitignore @@ -3,7 +3,7 @@ # compiled output dist tmp -/out-tsc +out-tsc # dependencies node_modules diff --git a/packages/workspace/src/generators/new/files-package-based-repo/__dot__gitignore b/packages/workspace/src/generators/new/files-package-based-repo/__dot__gitignore index 4f4d87b1ce..ccbf37187c 100644 --- a/packages/workspace/src/generators/new/files-package-based-repo/__dot__gitignore +++ b/packages/workspace/src/generators/new/files-package-based-repo/__dot__gitignore @@ -3,7 +3,7 @@ # compiled output dist tmp -/out-tsc +out-tsc # dependencies node_modules diff --git a/packages/workspace/src/generators/new/files-root-app/__dot__gitignore b/packages/workspace/src/generators/new/files-root-app/__dot__gitignore index 4f4d87b1ce..ccbf37187c 100644 --- a/packages/workspace/src/generators/new/files-root-app/__dot__gitignore +++ b/packages/workspace/src/generators/new/files-root-app/__dot__gitignore @@ -3,7 +3,7 @@ # compiled output dist tmp -/out-tsc +out-tsc # dependencies node_modules