feat(core): add migration to update workspace generators to a local plugin (#12700)

This commit is contained in:
Craigory Coppola 2023-04-19 14:03:00 -04:00 committed by GitHub
parent f04f316271
commit 1743ff10ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 539 additions and 666 deletions

View File

@ -5,7 +5,9 @@ description: 'Runs a workspace generator from the tools/generators directory'
# workspace-generator # workspace-generator
Runs a workspace generator from the tools/generators directory **Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators
Runs a workspace generator from the tools/generators directory
## Usage ## Usage
@ -17,23 +19,45 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
## Options ## Options
### dryRun
Type: `boolean`
Default: `false`
Preview the changes without updating files
### generator
Type: `string`
Name of the generator (e.g., @nrwl/js:library, library)
### help ### help
Type: `boolean` Type: `boolean`
Show help Show help
### list-generators ### interactive
Type: `boolean` Type: `boolean`
List the available workspace-generators Default: `true`
### name When false disables interactive input prompts for options
Type: `string` ### quiet
The name of your generator Type: `boolean`
Hides logs from tree operations (e.g. `CREATE package.json`)
### verbose
Type: `boolean`
Prints additional information about the commands (e.g., stack traces)
### version ### version

View File

@ -5,7 +5,9 @@ description: 'Runs a workspace generator from the tools/generators directory'
# workspace-generator # workspace-generator
Runs a workspace generator from the tools/generators directory **Deprecated:** Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators
Runs a workspace generator from the tools/generators directory
## Usage ## Usage
@ -17,23 +19,45 @@ Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`
## Options ## Options
### dryRun
Type: `boolean`
Default: `false`
Preview the changes without updating files
### generator
Type: `string`
Name of the generator (e.g., @nrwl/js:library, library)
### help ### help
Type: `boolean` Type: `boolean`
Show help Show help
### list-generators ### interactive
Type: `boolean` Type: `boolean`
List the available workspace-generators Default: `true`
### name When false disables interactive input prompts for options
Type: `string` ### quiet
The name of your generator Type: `boolean`
Hides logs from tree operations (e.g. `CREATE package.json`)
### verbose
Type: `boolean`
Prints additional information about the commands (e.g., stack traces)
### version ### version

View File

@ -7,20 +7,7 @@
"title": "Create a custom generator", "title": "Create a custom generator",
"description": "Create a custom generator.", "description": "Create a custom generator.",
"type": "object", "type": "object",
"properties": { "properties": {},
"name": {
"type": "string",
"description": "Generator name.",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What name would you like to use for the workspace generator?"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
}
},
"required": ["name"], "required": ["name"],
"presets": [] "presets": []
}, },

View File

@ -8,6 +8,10 @@ Check the [nx-plugin guide](/packages/nx-plugin) for information on creating a n
## Converting workspace generators to local generators ## Converting workspace generators to local generators
{% callout type=\"info\" %}
When migrating to Nx 16, a new workspace plugin is automatically generated in the tools folder if you already have workspace-generators.
{% /callout %}
- If you don't already have a local plugin, use Nx to generate one: - If you don't already have a local plugin, use Nx to generate one:
```shell ```shell

View File

@ -86,7 +86,6 @@ describe('Nx Commands', () => {
// check for schematics // check for schematics
expect(listOutput).toContain('workspace'); expect(listOutput).toContain('workspace');
expect(listOutput).toContain('library'); expect(listOutput).toContain('library');
expect(listOutput).toContain('workspace-generator');
// check for builders // check for builders
expect(listOutput).toContain('run-commands'); expect(listOutput).toContain('run-commands');

View File

@ -756,180 +756,3 @@ describe('Workspace Tests', () => {
}); });
}); });
}); });
describe('workspace-generator', () => {
const packageManager = getSelectedPackageManager() || 'pnpm';
const proj = uniq('workspace');
beforeAll(() => {
runCreateWorkspace(proj, {
preset: 'ts',
packageManager,
});
});
afterAll(() => cleanupProject());
let custom: string;
let failing: string;
beforeEach(() => {
custom = uniq('custom');
failing = uniq('custom-failing');
runCLI(`g @nrwl/workspace:workspace-generator ${custom} --no-interactive`);
runCLI(`g @nrwl/workspace:workspace-generator ${failing} --no-interactive`);
checkFilesExist(
`tools/generators/${custom}/index.ts`,
`tools/generators/${custom}/schema.json`
);
checkFilesExist(
`tools/generators/${failing}/index.ts`,
`tools/generators/${failing}/schema.json`
);
});
it('should compile only generator files with dependencies', () => {
const workspace = uniq('workspace');
updateFile(
'tools/utils/command-line-utils.ts',
`
export const noop = () => {}
`
);
updateFile(
'tools/utils/logger.ts',
`
export const log = (...args: any[]) => console.log(...args)
`
);
updateFile(
`tools/generators/utils.ts`,
`
export const noop = ()=>{}
`
);
updateFile(`tools/generators/${custom}/index.ts`, (content) => {
return `
import { log } from '../../utils/logger'; \n
${content}
`;
});
runCLI(`workspace-generator ${custom} ${workspace} --no-interactive -d`);
expect(() =>
checkFilesExist(
`dist/out-tsc/tools/generators/${custom}/index.js`,
`dist/out-tsc/tools/generators/utils.js`,
`dist/out-tsc/tools/utils/logger.js`
)
).not.toThrow();
expect(() =>
checkFilesExist(`dist/out-tsc/tools/utils/utils.js`)
).toThrow();
});
it('should support workspace-specific generators', async () => {
const json = readJson(`tools/generators/${custom}/schema.json`);
json.properties['directory'] = {
type: 'string',
description: 'lib directory',
};
json.properties['skipTsConfig'] = {
type: 'boolean',
description: 'skip changes to tsconfig',
};
json.properties['inlineprop'] = json.properties['name'];
json.required = ['inlineprop'];
delete json.properties['name'];
updateFile(`tools/generators/${custom}/schema.json`, JSON.stringify(json));
const indexFile = readFile(`tools/generators/${custom}/index.ts`);
updateFile(
`tools/generators/${custom}/index.ts`,
indexFile.replace(
'name: schema.name',
'name: schema.inlineprop, directory: schema.directory, skipTsConfig: schema.skipTsConfig'
)
);
const helpOutput = runCLI(`workspace-generator ${custom} --help`);
expect(helpOutput).toContain(
`workspace-generator ${custom} [inlineprop] (options)`
);
expect(helpOutput).toContain(`--directory`);
expect(helpOutput).toContain(`--skipTsConfig`);
const workspace = uniq('workspace');
const dryRunOutput = runCLI(
`workspace-generator ${custom} ${workspace} --no-interactive --directory=dir --skipTsConfig=true -d`
);
expect(exists(`packages/dir/${workspace}/src/index.ts`)).toEqual(false);
expect(dryRunOutput).toContain(
`CREATE packages/dir/${workspace}/src/index.ts`
);
runCLI(
`workspace-generator ${custom} ${workspace} --no-interactive --directory=dir`
);
checkFilesExist(`packages/dir/${workspace}/src/index.ts`);
const jsonFailing = readJson(`tools/generators/${failing}/schema.json`);
jsonFailing.properties = {};
jsonFailing.required = [];
updateFile(
`tools/generators/${failing}/schema.json`,
JSON.stringify(jsonFailing)
);
updateFile(
`tools/generators/${failing}/index.ts`,
`
export default function() {
throw new Error();
}
`
);
try {
await runCLI(`workspace-generator ${failing} --no-interactive`);
fail(`Should exit 1 for a workspace-generator that throws an error`);
} catch (e) {}
const listOutput = runCLI('workspace-generator --list-generators');
expect(listOutput).toContain(custom);
expect(listOutput).toContain(failing);
}, 1000000);
it('should support angular devkit schematics', () => {
const angularDevkitSchematic = uniq('angular-devkit-schematic');
runCLI(
`g @nrwl/workspace:workspace-generator ${angularDevkitSchematic} --no-interactive`
);
const json = readJson(
`tools/generators/${angularDevkitSchematic}/schema.json`
);
json.properties = {};
json.required = [];
delete json.cli;
updateFile(
`tools/generators/${angularDevkitSchematic}/schema.json`,
JSON.stringify(json)
);
updateFile(
`tools/generators/${angularDevkitSchematic}/index.ts`,
`
export default function() {
return (tree) => tree;
}
`
);
runCLI(`workspace-generator ${angularDevkitSchematic} --no-interactive`);
});
});

View File

@ -259,9 +259,6 @@ describe('Nx Plugin', () => {
expect(results).not.toContain(goodMigration); expect(results).not.toContain(goodMigration);
}); });
/**
* @todo(@AgentEnder): reenable after figuring out @swc-node
*/
describe('local plugins', () => { describe('local plugins', () => {
let plugin: string; let plugin: string;
beforeEach(() => { beforeEach(() => {
@ -368,6 +365,28 @@ describe('Nx Plugin', () => {
}); });
}); });
describe('workspace-generator', () => {
let custom: string;
it('should work with generate wrapper', () => {
custom = uniq('custom');
const project = uniq('generated-project');
runCLI(`g @nrwl/nx-plugin:plugin workspace-plugin --no-interactive`);
runCLI(
`g @nrwl/nx-plugin:generator ${custom} --project workspace-plugin --no-interactive`
);
runCLI(
`workspace-generator ${custom} --name ${project} --no-interactive`
);
expect(() => {
checkFilesExist(
`libs/${project}/src/index.ts`,
`libs/${project}/project.json`
);
});
});
});
describe('--directory', () => { describe('--directory', () => {
it('should create a plugin in the specified directory', () => { it('should create a plugin in the specified directory', () => {
const plugin = uniq('plugin'); const plugin = uniq('plugin');

View File

@ -23,8 +23,7 @@
"error", "error",
"@angular-devkit/architect", "@angular-devkit/architect",
"@angular-devkit/core", "@angular-devkit/core",
"@angular-devkit/schematics", "@angular-devkit/schematics"
"@nx/workspace"
] ]
} }
}, },

View File

@ -22,6 +22,7 @@ import { addTsLibDependencies } from '@nx/js/src/utils/typescript/add-tslib-depe
import { addSwcRegisterDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies'; import { addSwcRegisterDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies';
import type { Schema } from './schema'; import type { Schema } from './schema';
import { tsLibVersion } from '@nx/js/src/utils/versions';
const nxVersion = require('../../../package.json').version; const nxVersion = require('../../../package.json').version;
@ -87,6 +88,7 @@ export async function pluginGenerator(host: Tree, schema: Schema) {
addDependenciesToPackageJson( addDependenciesToPackageJson(
host, host,
{ {
tslib: tsLibVersion,
'@nx/devkit': nxVersion, '@nx/devkit': nxVersion,
}, },
{ {

View File

@ -266,16 +266,17 @@ export const commandsObject = yargs
process.exit(0); process.exit(0);
}, },
}) })
/**
* @deprecated(v17): Remove `workspace-generator in v17. Use local plugins.
*/
.command({ .command({
command: 'workspace-generator [name]', command: 'workspace-generator [name]',
describe: 'Runs a workspace generator from the tools/generators directory', describe: 'Runs a workspace generator from the tools/generators directory',
deprecated:
'Use a local plugin instead. See: https://nx.dev/deprecated/workspace-generators',
aliases: ['workspace-schematic [name]'], aliases: ['workspace-schematic [name]'],
builder: async (yargs) => builder: async (yargs) =>
linkToNxDevAndExamples( linkToNxDevAndExamples(withGenerateOptions(yargs), 'workspace-generator'),
await withWorkspaceGeneratorOptions(yargs, process.argv.slice(3)),
'workspace-generator'
),
handler: workspaceGeneratorHandler, handler: workspaceGeneratorHandler,
}) })
.command({ .command({
@ -824,142 +825,11 @@ function withRunOneOptions(yargs: yargs.Argv) {
} }
} }
type OptionArgumentDefinition = { /**
type: yargs.Options['type']; * @deprecated(v17): Remove `workspace-generator in v17. Use local plugins.
describe?: string; */
default?: any; async function workspaceGeneratorHandler(args: yargs.Arguments) {
choices?: yargs.Options['type'][]; await (await import('./workspace-generators')).workspaceGenerators(args);
demandOption?: boolean;
};
type WorkspaceGeneratorProperties = {
[name: string]:
| {
type: yargs.Options['type'];
description?: string;
default?: any;
enum?: yargs.Options['type'][];
demandOption?: boolean;
}
| {
type: yargs.PositionalOptionsType;
description?: string;
default?: any;
enum?: yargs.PositionalOptionsType[];
$default: {
$source: 'argv';
index: number;
};
};
};
function isPositionalProperty(
property: WorkspaceGeneratorProperties[keyof WorkspaceGeneratorProperties]
): property is { type: yargs.PositionalOptionsType } {
return property['$default']?.['$source'] === 'argv';
}
async function withWorkspaceGeneratorOptions(
yargs: yargs.Argv,
args: string[]
) {
// filter out only positional arguments
args = args.filter((a) => !a.startsWith('-'));
if (args.length) {
// this is an actual workspace generator
return withCustomGeneratorOptions(yargs, args[0]);
} else {
yargs
.option('list-generators', {
describe: 'List the available workspace-generators',
type: 'boolean',
})
.positional('name', {
type: 'string',
describe: 'The name of your generator',
});
/**
* Don't require `name` if only listing available
* schematics
*/
if ((await yargs.argv).listGenerators !== true) {
yargs.demandOption('name');
}
return yargs;
}
}
async function withCustomGeneratorOptions(
yargs: yargs.Argv,
generatorName: string
) {
const schema = (
await import('./workspace-generators')
).workspaceGeneratorSchema(generatorName);
const options = [];
const positionals = [];
Object.entries(
(schema.properties ?? {}) as WorkspaceGeneratorProperties
).forEach(([name, prop]) => {
const option: { name: string; definition: OptionArgumentDefinition } = {
name,
definition: {
describe: prop.description,
type: prop.type,
default: prop.default,
choices: prop.enum,
},
};
if (schema.required && schema.required.includes(name)) {
option.definition.demandOption = true;
}
options.push(option);
if (isPositionalProperty(prop)) {
positionals.push({
name,
definition: {
describe: prop.description,
type: prop.type,
choices: prop.enum,
},
});
}
});
let command = generatorName;
positionals.forEach(({ name }) => {
command += ` [${name}]`;
});
if (options.length) {
command += ' (options)';
}
yargs
.command({
// this is the default and only command
command,
describe: schema.description || '',
builder: (y) => {
options.forEach(({ name, definition }) => {
y.option(name, definition);
});
positionals.forEach(({ name, definition }) => {
y.positional(name, definition);
});
return linkToNxDevAndExamples(y, 'workspace-generator');
},
handler: workspaceGeneratorHandler,
})
.fail(() => void 0); // no action is needed on failure as Nx will handle it based on schema validation
return yargs;
}
async function workspaceGeneratorHandler() {
await (
await import('./workspace-generators')
).workspaceGenerators(process.argv.slice(3));
process.exit(0); process.exit(0);
} }

View File

@ -1,213 +1,35 @@
import * as chalk from 'chalk'; import yargs = require('yargs');
import { execSync } from 'child_process'; import { readNxJson } from '../config/configuration';
import { readdirSync, existsSync } from 'fs'; import { NxJsonConfiguration } from '../devkit-exports';
import { copySync, removeSync } from 'fs-extra'; import { NX_PREFIX } from '../utils/logger';
import * as path from 'path';
import * as yargsParser from 'yargs-parser';
import { workspaceRoot } from '../utils/workspace-root';
import { fileExists } from '../utils/fileutils';
import { output } from '../utils/output'; import { output } from '../utils/output';
import type { CompilerOptions } from 'typescript';
import { generate } from './generate';
import { readJsonFile, writeJsonFile } from '../utils/fileutils';
import { logger } from '../utils/logger';
import { getPackageManagerCommand } from '../utils/package-manager';
import { normalizePath } from '../utils/path';
import { parserConfiguration } from './nx-commands';
const rootDirectory = workspaceRoot; /**
const toolsDir = path.join(rootDirectory, 'tools'); * Wraps `workspace-generator` to invoke `generate`.
const generatorsDir = path.join(toolsDir, 'generators'); *
const toolsTsConfigPath = path.join(toolsDir, 'tsconfig.tools.json'); * @deprecated(v17): Remove `workspace-generator in v17. Use local plugins.
*/
export async function workspaceGenerators(args: yargs.Arguments) {
const generator = process.argv.slice(3);
type TsConfig = { output.warn({
extends: string; title: `${NX_PREFIX} Workspace Generators are no longer supported`,
compilerOptions: CompilerOptions; bodyLines: [
files?: string[]; 'Instead, Nx now supports executing generators or executors from ',
include?: string[]; 'local plugins. To run a generator from a local plugin, ',
exclude?: string[]; 'use `nx generate` like you would with any other generator.',
references?: Array<{ path: string }>; '',
}; 'For more information, see: https://nx.dev/deprecated/workspace-generators',
],
export async function workspaceGenerators(args: string[]) {
const outDir = compileTools();
const collectionFile = path.join(outDir, 'workspace-generators.json');
const parsedArgs = parseOptions(args, outDir, collectionFile);
if (parsedArgs.listGenerators) {
return listGenerators(collectionFile);
} else {
process.exitCode = await generate(process.cwd(), parsedArgs);
}
}
export function workspaceGeneratorSchema(name: string) {
const schemaFile = path.join(generatorsDir, name, 'schema.json');
if (fileExists(schemaFile)) {
return readJsonFile(schemaFile);
} else {
logger.error(`Cannot find schema for ${name}. Does the generator exist?`);
process.exit(1);
}
}
// compile tools
function compileTools() {
const toolsOutDir = getToolsOutDir();
removeSync(toolsOutDir);
compileToolsDir(toolsOutDir);
const generatorsOutDir = path.join(toolsOutDir, 'generators');
const collectionData = constructCollection();
writeJsonFile(
path.join(generatorsOutDir, 'workspace-generators.json'),
collectionData
);
return generatorsOutDir;
}
function getToolsOutDir() {
const outDir = toolsTsConfig().compilerOptions.outDir;
if (!outDir) {
logger.error(`${toolsTsConfigPath} must specify an outDir`);
process.exit(1);
}
return path.resolve(toolsDir, outDir);
}
function compileToolsDir(outDir: string) {
copySync(generatorsDir, path.join(outDir, 'generators'));
const tmpTsConfigPath = createTmpTsConfig(toolsTsConfigPath, {
include: [path.join(generatorsDir, '**/*.ts')],
}); });
const pmc = getPackageManagerCommand(); const nxJson: NxJsonConfiguration = readNxJson();
const tsc = `${pmc.exec} tsc`; const collection = nxJson.npmScope
try { ? `@${nxJson.npmScope}/workspace-plugin`
execSync(`${tsc} -p ${tmpTsConfigPath}`, { : 'workspace-plugin';
stdio: 'inherit',
cwd: rootDirectory, args._ = args._.slice(1);
}); args.generator = `${collection}:${generator}`;
} catch {
process.exit(1); return (await import('./generate')).generate(process.cwd(), args);
}
}
function constructCollection() {
const generators = {};
const schematics = {};
readdirSync(generatorsDir).forEach((c) => {
const childDir = path.join(generatorsDir, c);
if (existsSync(path.join(childDir, 'schema.json'))) {
const generatorOrSchematic = {
factory: `./${c}`,
schema: `./${normalizePath(path.join(c, 'schema.json'))}`,
description: `Schematic ${c}`,
};
const { isSchematic } = readJsonFile(path.join(childDir, 'schema.json'));
if (isSchematic) {
schematics[c] = generatorOrSchematic;
} else {
generators[c] = generatorOrSchematic;
}
}
});
return {
name: 'workspace-generators',
version: '1.0',
generators,
schematics,
};
}
function toolsTsConfig(): TsConfig {
return readJsonFile<TsConfig>(toolsTsConfigPath);
}
function listGenerators(collectionFile: string) {
try {
const bodyLines: string[] = [];
const collection = readJsonFile(collectionFile);
bodyLines.push(chalk.bold(chalk.green('WORKSPACE GENERATORS')));
bodyLines.push('');
bodyLines.push(
...Object.entries(collection.generators).map(
([schematicName, schematicMeta]: [string, any]) => {
return `${chalk.bold(schematicName)} : ${schematicMeta.description}`;
}
)
);
bodyLines.push('');
output.log({
title: '',
bodyLines,
});
} catch (error) {
logger.fatal(error.message);
}
}
function parseOptions(
args: string[],
outDir: string,
collectionFile: string
): { [k: string]: any } {
const schemaPath = path.join(outDir, args[0], 'schema.json');
let booleanProps = [];
if (fileExists(schemaPath)) {
const { properties } = readJsonFile(
path.join(outDir, args[0], 'schema.json')
);
if (properties) {
booleanProps = Object.keys(properties).filter(
(key) => properties[key].type === 'boolean'
);
}
}
const parsed = yargsParser(args, {
boolean: ['dryRun', 'listGenerators', 'interactive', ...booleanProps],
alias: {
dryRun: ['d'],
listSchematics: ['l'],
},
default: {
interactive: true,
},
configuration: parserConfiguration,
});
parsed['generator'] = `${collectionFile}:${parsed['_'][0]}`;
parsed['_'] = parsed['_'].slice(1);
return parsed;
}
function createTmpTsConfig(
tsconfigPath: string,
updateConfig: Partial<TsConfig>
) {
const tmpTsConfigPath = path.join(
path.dirname(tsconfigPath),
'tsconfig.generated.json'
);
const originalTSConfig = readJsonFile<TsConfig>(tsconfigPath);
const generatedTSConfig: TsConfig = {
...originalTSConfig,
...updateConfig,
};
process.on('exit', () => cleanupTmpTsConfigFile(tmpTsConfigPath));
writeJsonFile(tmpTsConfigPath, generatedTSConfig);
return tmpTsConfigPath;
}
function cleanupTmpTsConfigFile(tmpTsConfigPath: string) {
if (tmpTsConfigPath) {
removeSync(tmpTsConfigPath);
}
} }

View File

@ -89,6 +89,12 @@
"version": "16.0.0-beta.1", "version": "16.0.0-beta.1",
"description": "Replace @nrwl/workspace with @nx/workspace", "description": "Replace @nrwl/workspace with @nx/workspace",
"implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages" "implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages"
},
"16-0-0-move-workspace-generators-into-local-plugin": {
"version": "16.0.0-beta.3",
"description": "Generates a plugin called 'workspace-plugin' containing your workspace generators.",
"cli": "nx",
"implementation": "./src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -10,17 +10,21 @@ export function createProjectConfigurationInNewDestination(
schema: NormalizedSchema, schema: NormalizedSchema,
projectConfig: ProjectConfiguration projectConfig: ProjectConfiguration
) { ) {
if (projectConfig.name) { projectConfig.name = schema.newProjectName;
projectConfig.name = schema.newProjectName;
} // Subtle bug if project name === path, where the updated name was being overrideen.
const { name, ...rest } = projectConfig;
// replace old root path with new one // replace old root path with new one
const projectString = JSON.stringify(projectConfig); const projectString = JSON.stringify(rest);
const newProjectString = projectString.replace( const newProjectString = projectString.replace(
new RegExp(projectConfig.root, 'g'), new RegExp(projectConfig.root, 'g'),
schema.relativeToRootDestination schema.relativeToRootDestination
); );
const newProject: ProjectConfiguration = JSON.parse(newProjectString); const newProject: ProjectConfiguration = {
name,
...JSON.parse(newProjectString),
};
// Create a new project with the root replaced // Create a new project with the root replaced
addProjectConfiguration(tree, schema.newProjectName, newProject); addProjectConfiguration(tree, schema.newProjectName, newProject);

View File

@ -11,7 +11,8 @@ export function normalizeSchema(
const destination = schema.destination.startsWith('/') const destination = schema.destination.startsWith('/')
? normalizeSlashes(schema.destination.slice(1)) ? normalizeSlashes(schema.destination.slice(1))
: schema.destination; : schema.destination;
const newProjectName = getNewProjectName(destination); const newProjectName =
schema.newProjectName ?? getNewProjectName(destination);
const { npmScope } = getWorkspaceLayout(tree); const { npmScope } = getWorkspaceLayout(tree);
return { return {

View File

@ -110,7 +110,9 @@ export function updateImports(
if (schema.updateImportPath) { if (schema.updateImportPath) {
tsConfig.compilerOptions.paths[projectRef.to] = updatedPath; tsConfig.compilerOptions.paths[projectRef.to] = updatedPath;
delete tsConfig.compilerOptions.paths[projectRef.from]; if (projectRef.from !== projectRef.to) {
delete tsConfig.compilerOptions.paths[projectRef.from];
}
} else { } else {
tsConfig.compilerOptions.paths[projectRef.from] = updatedPath; tsConfig.compilerOptions.paths[projectRef.from] = updatedPath;
} }

View File

@ -19,6 +19,10 @@ export function getDestination(
schema: Schema, schema: Schema,
project: ProjectConfiguration project: ProjectConfiguration
): string { ): string {
if (schema.destinationRelativeToRoot) {
return schema.destination;
}
const projectType = project.projectType; const projectType = project.projectType;
const workspaceLayout = getWorkspaceLayout(host); const workspaceLayout = getWorkspaceLayout(host);

View File

@ -4,10 +4,11 @@ export interface Schema {
importPath?: string; importPath?: string;
updateImportPath: boolean; updateImportPath: boolean;
skipFormat?: boolean; skipFormat?: boolean;
destinationRelativeToRoot?: boolean;
newProjectName?: string;
} }
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
importPath: string; importPath: string;
newProjectName: string;
relativeToRootDestination: string; relativeToRootDestination: string;
} }

View File

@ -1,10 +0,0 @@
import { Tree, formatFiles, installPackagesTask } from '@nx/devkit';
import { libraryGenerator } from '@nx/js';
export default async function(tree: Tree, schema: any) {
await libraryGenerator(tree, {name: schema.name});
await formatFiles(tree);
return () => {
installPackagesTask(tree)
}
}

View File

@ -1,17 +0,0 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "<%= name %>",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": {
"$source": "argv",
"index": 0
}
}
},
"required": ["name"]
}

View File

@ -1,4 +1 @@
export interface Schema { export interface Schema {}
name: string;
skipFormat: boolean;
}

View File

@ -4,22 +4,6 @@
"title": "Create a custom generator", "title": "Create a custom generator",
"description": "Create a custom generator.", "description": "Create a custom generator.",
"type": "object", "type": "object",
"properties": { "properties": {},
"name": {
"type": "string",
"description": "Generator name.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the workspace generator?"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
}
},
"required": ["name"] "required": ["name"]
} }

View File

@ -1,17 +0,0 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import workspaceGenerator from './workspace-generator';
describe('workspace-generator', () => {
it('should generate a target', async () => {
const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const opts = {
name: 'custom',
skipFormat: true,
};
await workspaceGenerator(tree, opts);
expect(tree.exists('tools/generators/custom/index.ts')).toBeTruthy();
expect(tree.exists('tools/generators/custom/schema.json')).toBeTruthy();
});
});

View File

@ -1,42 +1,15 @@
import { Schema } from './schema'; import { Schema } from './schema';
import { import { Tree, stripIndents } from '@nx/devkit';
Tree,
formatFiles,
generateFiles,
names,
joinPathFragments,
addDependenciesToPackageJson,
} from '@nx/devkit';
import { nxVersion } from '../../utils/versions';
export default async function (host: Tree, schema: Schema) { export default async function (host: Tree, schema: Schema) {
const options = normalizeOptions(schema); const message = stripIndents`Workspace Generators are no longer supported. Instead,
Nx now supports executing generators or executors from local plugins. To get
started, install @nx/nx-plugin and run \`nx g plugin\`.
generateFiles( Afterwards, or if you already have an Nx plugin, you can run
host, \`nx g generator --project {my-plugin}\` to add a new generator.
joinPathFragments(__dirname, 'files'),
joinPathFragments('tools/generators', schema.name), For more information, see: https://nx.dev/deprecated/workspace-generators`;
options
);
const installTask = addDependenciesToPackageJson( throw new Error(message);
host,
{},
{
'@nx/devkit': nxVersion,
// types/node is neccessary for pnpm since it's used in tsconfig and transitive
// dependencies are not resolved correctly
'@types/node': 'latest',
}
);
if (!schema.skipFormat) {
await formatFiles(host);
}
return installTask;
}
function normalizeOptions(options: Schema): any {
const name = names(options.name).fileName;
return { ...options, name, tmpl: '' };
} }

View File

@ -0,0 +1,174 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
Tree,
readProjectConfiguration,
readJson,
joinPathFragments,
GeneratorsJson,
ProjectConfiguration,
stripIndents,
getProjects,
} from '@nx/devkit';
import generator from './move-workspace-generators-to-local-plugin';
describe('move-workspace-generators-to-local-plugin', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should find single workspace generator successfully', async () => {
await workspaceGeneratorGenerator(tree, {
name: 'my-generator',
});
await generator(tree);
console.log(getProjects(tree).keys());
const config = readProjectConfiguration(tree, 'workspace-plugin');
expect(config.root).toEqual('tools/workspace-plugin');
assertValidGenerator(tree, config, 'my-generator');
});
it('should convert multiple workspace generators successfully', async () => {
const generators = [...new Array(10)].map((x) => uniq('generator'));
for (const name of generators) {
await workspaceGeneratorGenerator(tree, {
name,
});
}
await generator(tree);
const config = readProjectConfiguration(tree, 'workspace-plugin');
expect(config.root).toEqual('tools/workspace-plugin');
for (const generator of generators) {
assertValidGenerator(tree, config, generator);
}
});
it('should be idempotent', async () => {
const generators = [...new Array(10)].map((x) => uniq('generator'));
for (const name of generators) {
await workspaceGeneratorGenerator(tree, {
name,
});
}
await generator(tree);
const generatorsJson = readJson(
tree,
'tools/workspace-plugin/generators.json'
);
await generator(tree);
expect(readJson(tree, 'tools/workspace-plugin/generators.json')).toEqual(
generatorsJson
);
const config = readProjectConfiguration(tree, 'workspace-plugin');
for (const generator of generators) {
assertValidGenerator(tree, config, generator);
}
});
it('should merge new generators into existing plugin', async () => {
const generators = [...new Array(10)].map((x) => uniq('generator'));
for (const name of generators) {
await workspaceGeneratorGenerator(tree, {
name,
});
}
await generator(tree);
const moreGenerators = [...new Array(5)].map((x) => uniq('generator'));
for (const name of moreGenerators) {
await workspaceGeneratorGenerator(tree, {
name,
});
}
await generator(tree);
const config = readProjectConfiguration(tree, 'workspace-plugin');
for (const generator of generators.concat(moreGenerators)) {
assertValidGenerator(tree, config, generator);
}
});
});
function assertValidGenerator(
tree: Tree,
config: ProjectConfiguration,
generator: string
) {
const generatorsJson = readJson<GeneratorsJson>(
tree,
joinPathFragments(config.root, 'generators.json')
);
expect(generatorsJson.generators).toHaveProperty(generator);
const generatorImplPath = joinPathFragments(
config.root,
generatorsJson.generators[generator].implementation,
'index.ts'
);
expect(tree.exists(generatorImplPath)).toBeTruthy();
const generatorSchemaPath = joinPathFragments(
config.root,
generatorsJson.generators[generator].schema
);
expect(tree.exists(generatorSchemaPath)).toBeTruthy();
}
function uniq(prefix: string) {
return `${prefix}${Math.floor(Math.random() * 10000000)}`;
}
async function workspaceGeneratorGenerator(
host: Tree,
schema: { name: string }
) {
const outputDirectory = joinPathFragments('tools/generators', schema.name);
host.write(
joinPathFragments(outputDirectory, 'index.ts'),
stripIndents`import { Tree, formatFiles, installPackagesTask } from '@nx/devkit';
import { libraryGenerator } from '@nx/workspace/generators';
export default async function(tree: Tree, schema: any) {
await libraryGenerator(tree, {name: schema.name});
await formatFiles(tree);
return () => {
installPackagesTask(tree)
}
}`
);
host.write(
joinPathFragments(outputDirectory, 'schema.json'),
stripIndents`{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "<%= name %>",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": {
"$source": "argv",
"index": 0
}
}
},
"required": ["name"]
}
`
);
}

View File

@ -0,0 +1,188 @@
import {
addDependenciesToPackageJson,
ensurePackage,
formatFiles,
getProjects,
getWorkspaceLayout,
joinPathFragments,
output,
ProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateJson,
writeJson,
} from '@nx/devkit';
// nx-ignore-next-line
import * as path from 'path';
import {
GeneratorsJson,
GeneratorsJsonEntry,
} from 'nx/src/config/misc-interfaces';
import { moveGenerator } from '../../generators/move/move';
import { nxVersion } from '../../utils/versions';
import { PackageJson } from 'nx/src/utils/package-json';
const PROJECT_NAME = 'workspace-plugin';
const DESTINATION = `tools/${PROJECT_NAME}`;
export default async function (tree: Tree) {
const tasks = [];
if (!tree.children('tools/generators').length) {
return;
}
let project = getProjects(tree).get(PROJECT_NAME);
if (!project) {
await createNewPlugin(tree);
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
'@nx/nx-plugin': nxVersion,
}
)
);
project = readProjectConfiguration(tree, PROJECT_NAME);
}
await updateExistingPlugin(tree, project);
await formatFiles(tree);
return () => {
for (const task of tasks) {
task();
}
};
}
// Inspired by packages/nx/src/command-line/workspace-generators.ts
function collectAndMoveGenerators(tree: Tree, destinationProjectRoot: string) {
const generators: Record<string, GeneratorsJsonEntry> = {};
const generatorsDir = 'tools/generators';
const destinationDir = joinPathFragments(
destinationProjectRoot,
'src',
'generators'
);
for (const c of tree.children('tools/generators')) {
const childDir = path.join(generatorsDir, c);
const schemaPath = joinPathFragments(childDir, 'schema.json');
if (tree.exists(schemaPath)) {
const schema = readJson(tree, schemaPath);
generators[c] = {
implementation: `./src/generators/${c}`,
schema: `./src/generators/${joinPathFragments(c, 'schema.json')}`,
description: schema.description ?? `Generator ${c}`,
};
tree.rename(childDir, joinPathFragments(destinationDir, c));
}
}
return generators;
}
async function createNewPlugin(tree: Tree) {
ensurePackage('@nx/nx-plugin', nxVersion);
const { pluginGenerator } =
// nx-ignore-next-line
require('@nx/nx-plugin/src/generators/plugin/plugin');
// nx-ignore-next-line
const { Linter } = ensurePackage('@nx/linter', nxVersion);
const { npmScope } = getWorkspaceLayout(tree);
const importPath = npmScope ? `${npmScope}/${PROJECT_NAME}` : PROJECT_NAME;
await pluginGenerator(tree, {
minimal: true,
name: PROJECT_NAME,
importPath: importPath,
skipTsConfig: false,
compiler: 'tsc',
linter: Linter.EsLint,
skipFormat: true,
skipLintChecks: false,
unitTestRunner: 'jest',
e2eTestRunner: 'none',
});
getCreateGeneratorsJson()(
tree,
readProjectConfiguration(tree, PROJECT_NAME).root,
PROJECT_NAME
);
await moveGeneratedPlugin(tree, DESTINATION, importPath);
}
function moveGeneratedPlugin(
tree: Tree,
destination: string,
importPath: string
) {
const config = readProjectConfiguration(tree, PROJECT_NAME);
if (config.root !== DESTINATION) {
return moveGenerator(tree, {
destination,
projectName: PROJECT_NAME,
newProjectName: PROJECT_NAME,
updateImportPath: true,
destinationRelativeToRoot: true,
importPath: importPath,
});
}
}
function updateExistingPlugin(tree: Tree, project: ProjectConfiguration) {
const packageJson = readJson<PackageJson>(
tree,
joinPathFragments(project.root, 'package.json')
);
const defaultGeneratorsPath = joinPathFragments(
project.root,
'generators.json'
);
let generatorsJsonPath =
packageJson.generators ||
packageJson.schematics ||
tree.exists(defaultGeneratorsPath)
? defaultGeneratorsPath
: null;
if (!generatorsJsonPath) {
getCreateGeneratorsJson()(
tree,
readProjectConfiguration(tree, PROJECT_NAME).root,
PROJECT_NAME
);
generatorsJsonPath = defaultGeneratorsPath;
}
updateJson<GeneratorsJson>(tree, generatorsJsonPath, (json) => {
const generators = collectAndMoveGenerators(tree, project.root);
json.generators ??= {};
for (const generator in generators) {
if (json.generators[generator]) {
output.warn({
title: `Generator ${generator} already exists in ${project.name}`,
bodyLines: [
'Since you have a generator with the same name in your plugin, the generator from workspace-generators has been discarded.',
],
});
} else {
json.generators[generator] = generators[generator];
}
}
return json;
});
}
function getCreateGeneratorsJson(): (
host: Tree,
projectRoot: string,
projectName: string,
skipLintChecks?: boolean,
skipFormat?: boolean
) => Promise<void> {
// We cant use `as typeof import('@nx/nx-plugin/src/generators/generator/generator');` here
// because it will cause a typescript error at build time.
const { createGeneratorsJson } =
// nx-ignore-next-line
require('@nx/nx-plugin/src/generators/generator/generator');
return createGeneratorsJson;
}

View File

@ -192,6 +192,16 @@ const IGNORE_MATCHES_BY_FILE: Record<string, string[]> = {
'../../packages/angular/src/migrations/update-12-3-0/update-storybook.ts' '../../packages/angular/src/migrations/update-12-3-0/update-storybook.ts'
), ),
], ],
'@nx/nx-plugin': [
join(
__dirname,
'../../packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts'
),
join(
__dirname,
'../../packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts'
),
],
}; };
export default async function getMissingDependencies( export default async function getMissingDependencies(