feat(nx-plugin): add support for the ts solution config setup to the @nx/plugin plugin (#28724)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

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

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2024-11-12 20:25:31 +01:00 committed by GitHub
parent 2d77495cc5
commit f7f26d847e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1010 additions and 452 deletions

View File

@ -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"],

View File

@ -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",

View File

@ -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"],

View File

@ -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",

View File

@ -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"

View File

@ -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(

View File

@ -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);

View File

@ -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;

View File

@ -3,7 +3,7 @@
preset: '<%= offsetFromRoot %>jest.preset.<%= presetExt %>',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],<% if(testEnvironment) { %>
testEnvironment: '<%= testEnvironment %>',<% } %>
coverageDirectory: '<%= offsetFromRoot %>coverage/<%= projectRoot %>',
coverageDirectory: '<%= coverageDirectory %>',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',

View File

@ -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') { %>

View File

@ -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: [
'<rootDir>/src/**/__tests__/**/*.[jt]s?(x)',
'<rootDir>/src/**/*(*.)@(spec|test).[jt]s?(x)',

View File

@ -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"],<% } %>

View File

@ -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: '<rootDir>/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') {

View File

@ -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(

View File

@ -29,4 +29,5 @@ export interface JestProjectSchema {
export type NormalizedJestProjectSchema = JestProjectSchema & {
rootProject: boolean;
isTsSolutionSetup: boolean;
};

View File

@ -57,5 +57,6 @@ export function normalizeOptions(
outputPath,
options.main.replace(`${projectRoot}/`, '').replace('.ts', '.js')
),
generatePackageJson: options.generatePackageJson ?? true,
};
}

View File

@ -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"],

View File

@ -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));

View File

@ -1561,6 +1561,7 @@ describe('lib', () => {
"name": "@proj/my-lib",
"nx": {
"name": "my-lib",
"projectType": "library",
"sourceRoot": "my-lib/src",
},
"private": true,

View File

@ -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':

View File

@ -37,6 +37,8 @@ export interface LibraryGeneratorSchema {
simpleName?: boolean;
addPlugin?: boolean;
useProjectJson?: boolean;
skipWorkspacesWarning?: boolean;
useTscExecutor?: boolean;
}
export interface NormalizedLibraryGeneratorOptions

View File

@ -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.
*/
/// <reference path="registry.d.ts" />
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.
*/
/// <reference path="registry.d.ts" />
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<ProjectConfiguration>(
tree,
'project.json'
));
} catch {
// if project.json doesn't exist, try package.json
const { name, nx } = readJson<PackageJson>(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 };
}

View File

@ -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<T> {
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);
}

View File

@ -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<FileInputOutput>;
root?: string;
sourceRoot?: string;

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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<PackageJson>(
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;
}
);

View File

@ -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(

View File

@ -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;
}

View File

@ -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"]

View File

@ -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<NormalizedSchema> {
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,
};
}

View File

@ -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<NormalizedSchema> {
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<PackageJson>(
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);
}

View File

@ -9,5 +9,7 @@ export interface Schema {
linter?: Linter | LinterType;
skipFormat?: boolean;
rootProject?: boolean;
useProjectJson?: boolean;
addPlugin?: boolean;
skipWorkspacesWarning?: boolean;
}

View File

@ -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"]

View File

@ -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,
});

View File

@ -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<PackageJson>(
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<PackageJson>(
@ -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<NormalizedSchema> {
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),
};
}

View File

@ -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 = {

View File

@ -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', () => {

View File

@ -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',
});

View File

@ -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<NormalizedSchema> {
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<PackageJson>(
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<PackageJson>(
@ -151,15 +172,16 @@ async function updateGeneratorJson(host: Tree, options: NormalizedSchema) {
updateJson<GeneratorsJson>(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.

View File

@ -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,
});

View File

@ -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<NormalizedSchema> {
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<PackageJson>(
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(

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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"]

View File

@ -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<NormalizedSchema> {
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,
};
}

View File

@ -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<PackageJson>(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;

View File

@ -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;
}

View File

@ -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"]

View File

@ -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))}`;
}

View File

@ -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": [

View File

@ -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`,
});
}

View File

@ -3,7 +3,7 @@
# compiled output
dist
tmp
/out-tsc
out-tsc
# dependencies
node_modules

View File

@ -3,7 +3,7 @@
# compiled output
dist
tmp
/out-tsc
out-tsc
# dependencies
node_modules

View File

@ -3,7 +3,7 @@
# compiled output
dist
tmp
/out-tsc
out-tsc
# dependencies
node_modules