diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts b/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts deleted file mode 100644 index ebed7552062481..00000000000000 --- a/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -import { - Tree, - readProjectConfiguration, - readJson, - joinPathFragments, - GeneratorsJson, - ProjectConfiguration, - getProjects, -} from '@nrwl/devkit'; - -import generator from './generator'; -import workspaceGeneratorGenerator from '@nrwl/workspace/src/generators/workspace-generator/workspace-generator'; -import { LocalPluginFromToolsGeneratorSchema } from './schema'; - -describe('local-plugin-from-tools generator', () => { - let tree: Tree; - - const createOptions = ( - overrides?: Partial - ): Partial => ({ - ...overrides, - }); - - beforeEach(() => { - tree = createTreeWithEmptyWorkspace(); - }); - - it('should find single workspace generator successfully', async () => { - await workspaceGeneratorGenerator(tree, { - name: 'my-generator', - skipFormat: false, - }); - await generator(tree, createOptions()); - 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, - skipFormat: false, - }); - } - - await generator(tree, createOptions()); - - const config = readProjectConfiguration(tree, 'workspace-plugin'); - expect(config.root).toEqual('tools/workspace-plugin'); - - for (const generator of generators) { - assertValidGenerator(tree, config, generator); - } - }); - - describe('--plugin-name', () => { - it('should obey project name', async () => { - await workspaceGeneratorGenerator(tree, { - name: 'my-generator', - skipFormat: false, - }); - await generator( - tree, - createOptions({ - pluginName: 'workspace-tools', - }) - ); - - const config = readProjectConfiguration(tree, 'workspace-tools'); - expect(config.root).toEqual('tools/workspace-tools'); - - assertValidGenerator(tree, config, 'my-generator'); - }); - }); - - describe('--tools-project-root', () => { - it('should place plugin in specified root', async () => { - await workspaceGeneratorGenerator(tree, { - name: 'my-generator', - skipFormat: false, - }); - await generator( - tree, - createOptions({ - toolsProjectRoot: 'libs/workspace-plugin', - }) - ); - const config = readProjectConfiguration(tree, 'workspace-plugin'); - expect(config.root).toEqual('libs/workspace-plugin'); - - assertValidGenerator(tree, config, 'my-generator'); - }); - }); - - describe('--import-path', () => { - it('should support basic import paths', async () => { - await workspaceGeneratorGenerator(tree, { - name: 'my-generator', - skipFormat: false, - }); - await generator( - tree, - createOptions({ - importPath: 'workspace-tools', - }) - ); - - const config = readProjectConfiguration(tree, 'workspace-plugin'); - expect(config.root).toEqual('tools/workspace-plugin'); - expect( - readJson(tree, 'tsconfig.base.json').compilerOptions.paths[ - 'workspace-tools' - ] - ).toEqual(['tools/workspace-plugin/src/index.ts']); - - assertValidGenerator(tree, config, 'my-generator'); - }); - - it('should support scoped import paths', async () => { - await workspaceGeneratorGenerator(tree, { - name: 'my-generator', - skipFormat: false, - }); - await generator( - tree, - createOptions({ - importPath: '@workspace/plugin', - }) - ); - - const config = readProjectConfiguration(tree, 'workspace-plugin'); - expect(config.root).toEqual('tools/workspace-plugin'); - expect( - readJson(tree, 'tsconfig.base.json').compilerOptions.paths[ - '@workspace/plugin' - ] - ).toEqual(['tools/workspace-plugin/src/index.ts']); - - assertValidGenerator(tree, config, 'my-generator'); - }); - }); -}); - -function assertValidGenerator( - tree: Tree, - config: ProjectConfiguration, - generator: string -) { - const generatorsJson = readJson( - 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)}`; -} diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts b/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts deleted file mode 100644 index 950b9150cc6f2d..00000000000000 --- a/packages/nx-plugin/src/generators/local-plugin-from-tools/generator.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - formatFiles, - generateFiles, - getWorkspaceLayout, - joinPathFragments, - readJson, - readProjectConfiguration, - Tree, -} from '@nrwl/devkit'; -import pluginGenerator from '../plugin/plugin'; -import * as path from 'path'; -import { LocalPluginFromToolsGeneratorSchema } from './schema'; -import { Linter } from '@nrwl/linter'; -import { GeneratorsJsonEntry } from 'nx/src/config/misc-interfaces'; -import { moveGenerator } from '@nrwl/workspace'; - -function normalizeOptions( - tree: Tree, - schema: Partial -): LocalPluginFromToolsGeneratorSchema { - const { npmScope } = getWorkspaceLayout(tree); - const pluginName = schema.pluginName ?? 'workspace-plugin'; - - return { - importPath: schema.importPath ?? `@${npmScope}/${pluginName}`, - pluginName, - toolsProjectRoot: - schema.toolsProjectRoot ?? joinPathFragments('tools', pluginName), - }; -} - -function addFiles( - tree: Tree, - options: LocalPluginFromToolsGeneratorSchema, - generators: (GeneratorsJsonEntry & { name: string })[] -) { - const templateOptions = { - ...options, - generators, - tmpl: '', - }; - generateFiles( - tree, - path.join(__dirname, 'files'), - options.toolsProjectRoot, - templateOptions - ); -} - -export default async function ( - tree: Tree, - options: Partial -) { - const normalizedOptions = normalizeOptions(tree, options); - await pluginGenerator(tree, { - minimal: true, - name: normalizedOptions.pluginName, - importPath: normalizedOptions.importPath, - skipTsConfig: false, - compiler: 'tsc', - linter: Linter.EsLint, - skipFormat: true, - skipLintChecks: false, - unitTestRunner: 'jest', - }); - await moveGeneratedPlugin(tree, normalizedOptions); - addFiles( - tree, - normalizedOptions, - collectAndMoveGenerators(tree, normalizedOptions) - ); - await formatFiles(tree); -} - -// Inspired by packages/nx/src/command-line/workspace-generators.ts -function collectAndMoveGenerators( - tree: Tree, - options: LocalPluginFromToolsGeneratorSchema -) { - const generators: ({ - name: string; - } & GeneratorsJsonEntry)[] = []; - const generatorsDir = 'tools/generators'; - const destinationDir = joinPathFragments( - readProjectConfiguration(tree, options.pluginName).root, - '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.push({ - name: c, - implementation: `./src/generators/${c}`, - schema: `./src/generators/${joinPathFragments(c, 'schema.json')}`, - description: schema.description ?? `Generator ${c}`, - }); - moveFolder(tree, childDir, joinPathFragments(destinationDir, c)); - } - } - return generators; -} - -function moveFolder(tree: Tree, source: string, destination: string) { - for (const child of tree.children(source)) { - const existingPath = joinPathFragments(source, child); - const newPath = joinPathFragments(destination, child); - if (tree.isFile(existingPath)) { - tree.write(newPath, tree.read(existingPath)); - tree.delete(existingPath); - } else { - moveFolder(tree, existingPath, newPath); - } - } - tree.delete(source); -} - -function moveGeneratedPlugin( - tree: Tree, - options: LocalPluginFromToolsGeneratorSchema -) { - const config = readProjectConfiguration(tree, options.pluginName); - if (config.root !== options.toolsProjectRoot) { - return moveGenerator(tree, { - destination: options.toolsProjectRoot, - projectName: options.pluginName, - newProjectName: options.pluginName, - updateImportPath: true, - destinationRelativeToRoot: true, - importPath: options.importPath, - }); - } -} diff --git a/packages/workspace/migrations.json b/packages/workspace/migrations.json index 16c87ae111895a..d8838c2f9db33b 100644 --- a/packages/workspace/migrations.json +++ b/packages/workspace/migrations.json @@ -83,6 +83,12 @@ "description": "Split global configuration files (e.g., workspace.json) into individual project.json files.", "cli": "nx", "implementation": "./src/migrations/update-15-7-0/split-configuration-into-project-json-files" + }, + "16-0-0-move-workspace-generators-into-local-plugin": { + "version": "16.0.0-beta.0", + "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.ts" } }, "packageJsonUpdates": { diff --git a/packages/nx-plugin/src/generators/local-plugin-from-tools/files/generators.json__tmpl__ b/packages/workspace/src/migrations/update-16-0-0/files/generators.json__tmpl__ similarity index 100% rename from packages/nx-plugin/src/generators/local-plugin-from-tools/files/generators.json__tmpl__ rename to packages/workspace/src/migrations/update-16-0-0/files/generators.json__tmpl__ diff --git a/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts new file mode 100644 index 00000000000000..df68a611a8bc14 --- /dev/null +++ b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.spec.ts @@ -0,0 +1,135 @@ +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { + Tree, + readProjectConfiguration, + readJson, + joinPathFragments, + GeneratorsJson, + ProjectConfiguration, + getProjects, +} from '@nrwl/devkit'; + +import generator from './move-workspace-generators-to-local-plugin'; +import workspaceGeneratorGenerator from '@nrwl/workspace/src/generators/workspace-generator/workspace-generator'; + +describe('local-plugin-from-tools generator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should find single workspace generator successfully', async () => { + await workspaceGeneratorGenerator(tree, { + name: 'my-generator', + skipFormat: false, + }); + await generator(tree); + 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, + skipFormat: false, + }); + } + + 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, + skipFormat: false, + }); + } + + 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, + skipFormat: false, + }); + } + + await generator(tree); + + const moreGenerators = [...new Array(5)].map((x) => uniq('generator')); + for (const name of moreGenerators) { + await workspaceGeneratorGenerator(tree, { + name, + skipFormat: false, + }); + } + + 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( + 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)}`; +} diff --git a/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts new file mode 100644 index 00000000000000..5a2b5e4faa9533 --- /dev/null +++ b/packages/workspace/src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin.ts @@ -0,0 +1,161 @@ +import { + ensurePackage, + formatFiles, + generateFiles, + getProjects, + getWorkspaceLayout, + joinPathFragments, + ProjectConfiguration, + readJson, + readProjectConfiguration, + Tree, + writeJson, +} from '@nrwl/devkit'; +import type * as pluginGeneratorModule from '@nrwl/nx-plugin/src/generators/plugin/plugin'; +import * as path from 'path'; +import { Linter } from '@nrwl/linter'; +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}`; + +function addFiles( + tree: Tree, + generators: (GeneratorsJsonEntry & { name: string })[], + destination: string, + importPath: string +) { + const templateOptions = { + generators, + tmpl: '', + importPath, + }; + generateFiles( + tree, + path.join(__dirname, 'files'), + destination, + templateOptions + ); +} + +export default async function (tree: Tree) { + if (!tree.children('tools/generators').length) { + return; + } + + const project = getProjects(tree).get(PROJECT_NAME); + if (project) { + await updateExistingPlugin(tree, project); + } else { + await createNewPlugin(tree); + } + await formatFiles(tree); +} + +// Inspired by packages/nx/src/command-line/workspace-generators.ts +function collectAndMoveGenerators(tree: Tree, destinationProjectRoot: string) { + const generators: ({ + name: 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.push({ + name: 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('@nrwl/nx-plugin', nxVersion); + const { pluginGenerator } = + require('@nrwl/nx-plugin/src/generators/plugin/plugin') as typeof pluginGeneratorModule; + + 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', + }); + await moveGeneratedPlugin(tree, DESTINATION, importPath); + addFiles( + tree, + collectAndMoveGenerators(tree, DESTINATION), + 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( + tree, + joinPathFragments(project.root, 'package.json') + ); + const defaultGeneratorsPath = joinPathFragments( + project.root, + 'generators.json' + ); + const generatorsJsonPath = + packageJson.generators || + packageJson.schematics || + tree.exists(defaultGeneratorsPath) + ? defaultGeneratorsPath + : null; + if (!generatorsJsonPath) { + throw new Error('Unable to locate generators.json for ' + project.name); + } + const generatorsJson = readJson(tree, generatorsJsonPath); + const generators = collectAndMoveGenerators(tree, project.root); + generatorsJson.generators ??= {}; + for (const { name, ...entry } of generators) { + generatorsJson.generators[name] = entry; + } + writeJson(tree, generatorsJsonPath, generatorsJson); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 6a85841df63c81..b0144fe73ba9a2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -88,6 +88,7 @@ "@nrwl/nx-dev/ui-sponsor-card": ["nx-dev/ui-sponsor-card/src/index.ts"], "@nrwl/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"], "@nrwl/nx-plugin": ["packages/nx-plugin"], + "@nrwl/nx-plugin/*": ["packages/nx-plugin/*"], "@nrwl/react": ["packages/react"], "@nrwl/react-native": ["packages/react-native"], "@nrwl/react/*": ["packages/react/*"],