feat(core): update create-nx-plugin to generate cli library (#15994)

This commit is contained in:
Emily Xiong 2023-04-19 20:36:11 -04:00 committed by GitHub
parent 16e4061b5d
commit 840048480f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 946 additions and 103 deletions

View File

@ -5579,6 +5579,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "create-package",
"path": "/packages/nx-plugin/generators/create-package",
"name": "create-package",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "e2e-project",
"path": "/packages/nx-plugin/generators/e2e-project",

View File

@ -1810,6 +1810,15 @@
"path": "/packages/nx-plugin/generators/plugin",
"type": "generator"
},
"/packages/nx-plugin/generators/create-package": {
"description": "Create a package which can be used by npx to create a new workspace",
"file": "generated/packages/nx-plugin/generators/create-package.json",
"hidden": false,
"name": "create-package",
"originalFilePath": "/packages/nx-plugin/src/generators/create-package/schema.json",
"path": "/packages/nx-plugin/generators/create-package",
"type": "generator"
},
"/packages/nx-plugin/generators/e2e-project": {
"description": "Create a E2E application for a Nx Plugin.",
"file": "generated/packages/nx-plugin/generators/e2e-project.json",

View File

@ -1788,6 +1788,15 @@
"path": "nx-plugin/generators/plugin",
"type": "generator"
},
{
"description": "Create a package which can be used by npx to create a new workspace",
"file": "generated/packages/nx-plugin/generators/create-package.json",
"hidden": false,
"name": "create-package",
"originalFilePath": "/packages/nx-plugin/src/generators/create-package/schema.json",
"path": "nx-plugin/generators/create-package",
"type": "generator"
},
{
"description": "Create a E2E application for a Nx Plugin.",
"file": "generated/packages/nx-plugin/generators/e2e-project.json",

View File

@ -117,7 +117,8 @@
"runInBand": {
"alias": "i",
"description": "Run all tests serially in the current process (rather than creating a worker pool of child processes that run tests). This is sometimes useful for debugging, but such use cases are pretty rare. Useful for CI. (https://jestjs.io/docs/cli#--runinband)",
"type": "boolean"
"type": "boolean",
"default": true
},
"showConfig": {
"description": "Print your Jest config and then exits. (https://jestjs.io/docs/en/cli#--showconfig)",

View File

@ -0,0 +1,75 @@
{
"name": "create-package",
"factory": "./src/generators/create-package/create-package",
"schema": {
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxPluginCreatePackage",
"title": "Create a framework package",
"description": "Create a framework package that uses Nx CLI.",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The package name of cli, e.g. `create-framework-package`. Note this must be a valid NPM name to be published.",
"$default": { "$source": "argv", "index": 0 },
"x-priority": "important"
},
"project": {
"type": "string",
"description": "The name of the generator project.",
"alias": "p",
"$default": { "$source": "projectName" },
"x-prompt": "What is the name of the project for the generator?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
},
"directory": {
"type": "string",
"description": "A directory where the app is placed."
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
},
"tags": {
"type": "string",
"description": "Add tags to the library (used for linting).",
"alias": "t"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"compiler": {
"type": "string",
"enum": ["tsc", "swc"],
"default": "tsc",
"description": "The compiler used by the build and test targets."
},
"e2eTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for end to end (E2E) tests.",
"default": "jest"
}
},
"required": ["name", "project"],
"presets": []
},
"description": "Create a package which can be used by npx to create a new workspace",
"implementation": "/packages/nx-plugin/src/generators/create-package/create-package.ts",
"aliases": [],
"hidden": false,
"path": "/packages/nx-plugin/src/generators/create-package/schema.json",
"type": "generator"
}

View File

@ -49,6 +49,12 @@
"type": "boolean",
"default": false,
"description": "Do not add an eslint configuration for plugin json files."
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files.",
"default": false,
"x-priority": "internal"
}
},
"required": ["project", "name"],

View File

@ -13,6 +13,10 @@
"type": "string",
"description": "Plugin name",
"aliases": ["name"]
},
"createPackageName": {
"type": "string",
"description": "Name of package which creates a workspace"
}
},
"required": ["pluginName"],

View File

@ -92,6 +92,7 @@
"enum": ["cypress", "jest", "detox", "none"]
}
},
"required": ["preset", "name"],
"presets": []
},
"description": "Create application in an empty workspace.",

View File

@ -5,7 +5,6 @@ import {
createFile,
expectTestsPass,
getPackageManagerCommand,
killPorts,
newProject,
readJson,
readProjectConfig,
@ -410,4 +409,31 @@ describe('Nx Plugin', () => {
expect(pluginProject.tags).toEqual(['e2etag', 'e2ePackage']);
}, 90000);
});
it('should be able to generate a create-package plugin ', async () => {
const plugin = uniq('plugin');
const createAppName = `create-${plugin}-app`;
runCLI(`generate @nrwl/nx-plugin:plugin ${plugin}`);
runCLI(
`generate @nrwl/nx-plugin:create-package ${createAppName} --project=${plugin}`
);
const buildResults = runCLI(`build ${createAppName}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`libs/${plugin}/src/generators/preset`,
`libs/${createAppName}`,
`dist/libs/${createAppName}/bin/index.js`
);
});
it('should throw an error when run create-package for an invalid plugin ', async () => {
const plugin = uniq('plugin');
expect(() =>
runCLI(
`generate @nrwl/nx-plugin:create-package ${plugin} --project=invalid-plugin`
)
).toThrow();
});
});

View File

@ -21,6 +21,7 @@ describe('create-nx-plugin', () => {
runCreatePlugin(pluginName, {
packageManager,
extraArgs: `--createPackageName='false'`,
});
checkFilesExist(
@ -31,7 +32,7 @@ describe('create-nx-plugin', () => {
runCLI(`build ${pluginName}`);
checkFilesExist(`dist/package.json`);
checkFilesExist(`dist/package.json`, `dist/src/index.js`);
runCLI(
`generate @nrwl/nx-plugin:generator ${generatorName} --project=${pluginName}`
@ -48,4 +49,22 @@ describe('create-nx-plugin', () => {
`dist/executors.json`
);
});
it('should be able to create a repo with create workspace cli', () => {
const pluginName = uniq('plugin');
runCreatePlugin(pluginName, {
packageManager,
extraArgs: `--createPackageName=create-${pluginName}-package`,
});
runCLI(`build ${pluginName}`);
checkFilesExist(`dist/package.json`, `dist/generators.json`);
runCLI(`build create-${pluginName}-package`);
checkFilesExist(`dist/create-${pluginName}-package/bin/index.js`);
expect(() => runCLI(`e2e e2e`)).not.toThrow();
expect(() => runCLI(`e2e create-${pluginName}-package-e2e`)).not.toThrow();
});
});

View File

@ -43,34 +43,44 @@ export const yargsDecorator = {
const nxVersion = require('../package.json').version;
function determinePluginName(parsedArgs: CreateNxPluginArguments) {
async function determinePluginName(
parsedArgs: CreateNxPluginArguments
): Promise<string> {
if (parsedArgs.pluginName) {
return Promise.resolve(parsedArgs.pluginName);
return parsedArgs.pluginName;
}
return enquirer
.prompt([
{
name: 'pluginName',
message: `Plugin name `,
type: 'input',
validate: (s) => (s.length ? true : 'Name cannot be empty'),
},
])
.then((a: { pluginName: string }) => {
if (!a.pluginName) {
output.error({
title: 'Invalid name',
bodyLines: [`Name cannot be empty`],
});
process.exit(1);
}
return a.pluginName;
});
const results = await enquirer.prompt<{ pluginName: string }>([
{
name: 'pluginName',
message: `Plugin name `,
type: 'input',
validate: (s_1) => (s_1.length ? true : 'Plugin name cannot be empty'),
},
]);
return results.pluginName;
}
async function determineCreatePackageName(
parsedArgs: CreateNxPluginArguments
): Promise<string> {
if (parsedArgs.createPackageName) {
return parsedArgs.createPackageName;
}
const results = await enquirer.prompt<{ createPackageName: string }>([
{
name: 'createPackageName',
message: `Create a package which can be used by npx to create a new workspace (Leave blank to not create this package)`,
type: 'input',
},
]);
return results.createPackageName;
}
interface CreateNxPluginArguments {
pluginName: string;
createPackageName?: string;
packageManager: PackageManager;
ci: CI;
allPrompts: boolean;
@ -89,11 +99,16 @@ export const commandsObject: yargs.Argv<CreateNxPluginArguments> = yargs
'Create a new Nx plugin workspace',
(yargs) =>
withOptions(
yargs.positional('pluginName', {
describe: chalk.dim`Plugin name`,
type: 'string',
alias: ['name'],
}),
yargs
.positional('pluginName', {
describe: chalk.dim`Plugin name`,
type: 'string',
alias: ['name'],
})
.option('createPackageName', {
describe: 'Name of the CLI package to create workspace with plugin',
type: 'string',
}),
withNxCloud,
withCI,
withAllPrompts,
@ -164,19 +179,21 @@ async function normalizeArgsMiddleware(
argv: yargs.Arguments<CreateNxPluginArguments>
): Promise<void> {
try {
const name = await determinePluginName(argv);
const pluginName = await determinePluginName(argv);
const createPackageName = await determineCreatePackageName(argv);
const packageManager = await determinePackageManager(argv);
const defaultBase = await determineDefaultBase(argv);
const nxCloud = await determineNxCloud(argv);
const ci = await determineCI(argv, nxCloud);
Object.assign(argv, {
name,
pluginName,
createPackageName,
nxCloud,
packageManager,
defaultBase,
ci,
});
} as Partial<CreateNxPluginArguments & CreateWorkspaceOptions>);
} catch (e) {
console.error(e);
process.exit(1);

View File

@ -221,6 +221,7 @@ async function normalizeArgsMiddleware(
} else if (monorepoStyle === 'node-standalone') {
preset = Preset.NodeStandalone;
} else {
// when choose integrated monorepo, further prompt for preset
preset = await determinePreset(argv);
}
} else if (argv.preset === 'react') {

View File

@ -27,8 +27,6 @@
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
},
"main": "./index.js",
"typings": "./index.d.ts",
"homepage": "https://nx.dev",
"dependencies": {
"chalk": "^4.1.0",

View File

@ -8,7 +8,7 @@
"build-base": {
"executor": "@nrwl/js:tsc",
"options": {
"main": "packages/create-nx-workspace/bin/create-nx-workspace.ts",
"main": "packages/create-nx-workspace/index.ts",
"assets": [
{
"input": "packages/create-nx-workspace",

View File

@ -39,7 +39,9 @@ export async function createPreset<T extends CreateWorkspaceOptions>(
}
}
if (process.env.NX_VERBOSE_LOGGING !== 'true') {
if (
!(process.env.NX_VERBOSE_LOGGING === 'true' || args.includes('--verbose'))
) {
args = '--quiet ' + args;
}
const command = `g ${preset}:preset ${args}`;

View File

@ -1,9 +1,11 @@
import { PackageManager } from './utils/package-manager';
import { CI } from './utils/ci/ci-list';
export interface CreateWorkspaceOptions {
name: string; // Workspace name (e.g. org name)
packageManager: PackageManager; // Package manager to use
nxCloud: boolean; // Enable Nx Cloud
presetVersion?: string; // Version of the preset to use
/**
* @description Enable interactive mode with presets
* @default true

View File

@ -60,7 +60,7 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
nxCloud && nxCloudInstallRes?.code === 0
);
}
if (!skipGit) {
if (!skipGit && commit) {
try {
await initializeGitRepo(directory, { defaultBase, commit });
} catch (e) {

View File

@ -5,18 +5,19 @@ import { isKnownPreset } from './preset';
/**
* This function is used to check if a preset is a third party preset.
* @param preset
* @returns null if the preset is a known Nx preset or preset does not exist, the normalized preset otherwise.
* @returns null if the preset is a known Nx preset or preset does not exist, the package name of preset otherwise.
*/
export async function getThirdPartyPreset(
preset?: string
): Promise<string | null> {
if (preset && !isKnownPreset(preset)) {
// extract the package name from the preset
const packageName = preset.match(/.+@/)
? preset[0] + preset.substring(1).split('@')[0]
: preset;
const validateResult = validateNpmPackage(packageName);
if (validateResult.validForNewPackages) {
return Promise.resolve(preset);
return Promise.resolve(packageName);
} else {
//! Error here
output.error({

View File

@ -2,7 +2,7 @@ import { addDependenciesToPackageJson, Tree } from '@nx/devkit';
import { tsLibVersion } from '../versions';
export function addTsLibDependencies(tree: Tree) {
addDependenciesToPackageJson(
return addDependenciesToPackageJson(
tree,
{
tslib: tsLibVersion,

View File

@ -8,6 +8,11 @@
"schema": "./src/generators/plugin/schema.json",
"description": "Create a Nx Plugin."
},
"create-package": {
"factory": "./src/generators/create-package/create-package",
"schema": "./src/generators/create-package/schema.json",
"description": "Create a package which can be used by npx to create a new workspace"
},
"e2e-project": {
"factory": "./src/generators/e2e-project/e2e",
"schema": "./src/generators/e2e-project/schema.json",
@ -47,6 +52,11 @@
"schema": "./src/generators/plugin/schema.json",
"description": "Create a Nx Plugin."
},
"create-package": {
"factory": "./src/generators/create-package/create-package#createPackageSchematic",
"schema": "./src/generators/create-package/schema.json",
"description": "Create a package which can be used by npx to create a new workspace"
},
"e2e-project": {
"factory": "./src/generators/e2e-project/e2e#e2eProjectSchematic",
"schema": "./src/generators/e2e-project/schema.json",

View File

@ -1,3 +1,4 @@
export * from './src/generators/create-package/create-package';
export * from './src/generators/e2e-project/e2e';
export * from './src/generators/executor/executor';
export * from './src/generators/generator/generator';

View File

@ -128,7 +128,8 @@
"runInBand": {
"alias": "i",
"description": "Run all tests serially in the current process (rather than creating a worker pool of child processes that run tests). This is sometimes useful for debugging, but such use cases are pretty rare. Useful for CI. (https://jestjs.io/docs/cli#--runinband)",
"type": "boolean"
"type": "boolean",
"default": true
},
"showConfig": {
"description": "Print your Jest config and then exits. (https://jestjs.io/docs/en/cli#--showconfig)",

View File

@ -0,0 +1,119 @@
import {
joinPathFragments,
readJson,
readProjectConfiguration,
Tree,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import { PackageJson } from 'nx/src/utils/package-json';
import pluginGenerator from '../plugin/plugin';
import { createPackageGenerator } from './create-package';
import { CreatePackageSchema } from './schema';
const getSchema: (
overrides?: Partial<CreatePackageSchema>
) => CreatePackageSchema = (overrides = {}) => ({
name: 'create-a-workspace',
project: 'my-plugin',
compiler: 'tsc',
skipTsConfig: false,
skipFormat: false,
skipLintChecks: false,
linter: Linter.EsLint,
unitTestRunner: 'jest',
...overrides,
});
describe('NxPlugin Create Package Generator', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
await pluginGenerator(tree, {
name: 'my-plugin',
compiler: 'tsc',
skipTsConfig: false,
skipFormat: false,
skipLintChecks: false,
linter: Linter.EsLint,
unitTestRunner: 'jest',
});
});
it('should update the project.json file', async () => {
await createPackageGenerator(tree, getSchema());
const project = readProjectConfiguration(tree, 'create-a-workspace');
expect(project.root).toEqual('libs/create-a-workspace');
expect(project.sourceRoot).toEqual('libs/create-a-workspace/bin');
expect(project.targets.build).toEqual({
executor: '@nx/js:tsc',
outputs: ['{options.outputPath}'],
options: {
outputPath: 'dist/libs/create-a-workspace',
tsConfig: 'libs/create-a-workspace/tsconfig.lib.json',
main: 'libs/create-a-workspace/bin/index.ts',
assets: ['libs/create-a-workspace/*.md'],
updateBuildableProjectDepsInPackageJson: false,
},
});
});
it('should place the create-package plugin in a directory', async () => {
await createPackageGenerator(
tree,
getSchema({
directory: 'plugins',
} as Partial<CreatePackageSchema>)
);
const project = readProjectConfiguration(
tree,
'plugins-create-a-workspace'
);
expect(project.root).toEqual('libs/plugins/create-a-workspace');
});
it('should specify tsc as compiler', async () => {
await createPackageGenerator(
tree,
getSchema({
compiler: 'tsc',
})
);
const { build } = readProjectConfiguration(
tree,
'create-a-workspace'
).targets;
expect(build.executor).toEqual('@nx/js:tsc');
});
it('should specify swc as compiler', async () => {
await createPackageGenerator(
tree,
getSchema({
compiler: 'swc',
})
);
const { build } = readProjectConfiguration(
tree,
'create-a-workspace'
).targets;
expect(build.executor).toEqual('@nx/js:swc');
});
it("should use name as default for the package.json's name", async () => {
await createPackageGenerator(tree, getSchema());
const { root } = readProjectConfiguration(tree, 'create-a-workspace');
const { name } = readJson<PackageJson>(
tree,
joinPathFragments(root, 'package.json')
);
expect(name).toEqual('create-a-workspace');
});
});

View File

@ -0,0 +1,211 @@
import {
addDependenciesToPackageJson,
readProjectConfiguration,
Tree,
generateFiles,
readJson,
convertNxGenerator,
formatFiles,
updateProjectConfiguration,
updateJson,
GeneratorCallback,
runTasksInSerial,
joinPathFragments,
} from '@nx/devkit';
import { libraryGenerator as jsLibraryGenerator } from '@nx/js';
import { nxVersion } from 'nx/src/utils/versions';
import generatorGenerator from '../generator/generator';
import { CreatePackageSchema } from './schema';
import { NormalizedSchema, normalizeSchema } from './utils/normalize-schema';
import e2eProjectGenerator from '../e2e-project/e2e';
import { hasGenerator } from '../../utils/has-generator';
export async function createPackageGenerator(
host: Tree,
schema: CreatePackageSchema
) {
const tasks: GeneratorCallback[] = [];
const options = normalizeSchema(host, schema);
const pluginPackageName = await addPresetGenerator(host, options);
const installTask = addDependenciesToPackageJson(
host,
{
'create-nx-workspace': nxVersion,
},
{}
);
tasks.push(installTask);
await createCliPackage(host, options, pluginPackageName);
if (options.e2eTestRunner !== 'none') {
tasks.push(await addE2eProject(host, options));
}
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(...tasks);
}
/**
* Add a preset generator to the plugin if it doesn't exist
* @param host
* @param schema
* @returns package name of the plugin
*/
async function addPresetGenerator(
host: Tree,
schema: NormalizedSchema
): Promise<string> {
const { root: projectRoot } = readProjectConfiguration(host, schema.project);
if (!hasGenerator(host, schema.project, 'preset')) {
await generatorGenerator(host, {
name: 'preset',
project: schema.project,
unitTestRunner: schema.unitTestRunner,
skipFormat: true,
});
}
return readJson(host, joinPathFragments(projectRoot, 'package.json'))?.name;
}
async function createCliPackage(
host: Tree,
options: NormalizedSchema,
pluginPackageName: string
) {
await jsLibraryGenerator(host, {
...options,
rootProject: false,
config: 'project',
publishable: true,
bundler: options.bundler,
importPath: options.name,
skipFormat: true,
skipTsConfig: true,
});
host.delete(joinPathFragments(options.projectRoot, 'src'));
// Add the bin entry to the package.json
updateJson(
host,
joinPathFragments(options.projectRoot, 'package.json'),
(packageJson) => {
packageJson.bin = {
[options.name]: './bin/index.js',
};
packageJson.dependencies = {
'create-nx-workspace': nxVersion,
};
return packageJson;
}
);
// update project build target to use the bin entry
const projectConfiguration = readProjectConfiguration(
host,
options.projectName
);
projectConfiguration.sourceRoot = joinPathFragments(
options.projectRoot,
'bin'
);
projectConfiguration.targets.build.options.main = joinPathFragments(
options.projectRoot,
'bin/index.ts'
);
projectConfiguration.targets.build.options.updateBuildableProjectDepsInPackageJson =
false;
projectConfiguration.implicitDependencies = [options.project];
updateProjectConfiguration(host, options.projectName, projectConfiguration);
// Add bin files to tsconfg.lib.json
updateJson(
host,
joinPathFragments(options.projectRoot, 'tsconfig.lib.json'),
(tsConfig) => {
tsConfig.include.push('bin/**/*.ts');
return tsConfig;
}
);
generateFiles(
host,
joinPathFragments(__dirname, './files/create-framework-package'),
options.projectRoot,
{
...options,
preset: pluginPackageName,
tmpl: '',
}
);
}
/**
* Add a test file to plugin e2e project
* @param host
* @param options
* @returns
*/
async function addE2eProject(host: Tree, options: NormalizedSchema) {
const pluginProjectConfiguration = readProjectConfiguration(
host,
options.project
);
const pluginOutputPath =
pluginProjectConfiguration.targets.build.options.outputPath;
const cliProjectConfiguration = readProjectConfiguration(
host,
options.projectName
);
const cliOutputPath =
cliProjectConfiguration.targets.build.options.outputPath;
const e2eTask = await e2eProjectGenerator(host, {
pluginName: options.projectName,
projectDirectory: options.projectDirectory,
pluginOutputPath,
npmPackageName: options.name,
skipFormat: true,
rootProject: false,
});
const e2eProjectConfiguration = readProjectConfiguration(
host,
`${options.projectName}-e2e`
);
e2eProjectConfiguration.targets.e2e.dependsOn = ['^build'];
updateProjectConfiguration(
host,
e2eProjectConfiguration.name,
e2eProjectConfiguration
);
// delete the default e2e test file
host.delete(e2eProjectConfiguration.sourceRoot);
generateFiles(
host,
joinPathFragments(__dirname, './files/e2e'),
e2eProjectConfiguration.sourceRoot,
{
...options,
pluginOutputPath,
cliOutputPath,
tmpl: '',
}
);
return e2eTask;
}
export default createPackageGenerator;
export const createPackageSchematic = convertNxGenerator(
createPackageGenerator
);

View File

@ -0,0 +1,26 @@
#!/usr/bin/env node
import { createWorkspace } from 'create-nx-workspace';
async function main() {
const name = process.argv[2]; // TODO: use libraries like yargs or enquirer to set your workspace name
if (!name) {
throw new Error('Please provide a name for the workspace');
}
console.log(`Creating the workspace: ${name}`);
// TODO: update below to customize the workspace
const { directory } = await createWorkspace('<%= preset %>', {
name,
nxCloud: false,
packageManager: 'npm',
// This assumes "<%= preset %>" and "<%= projectName %>" are at the same version
// eslint-disable-next-line @typescript-eslint/no-var-requires
presetVersion: require('../package.json').version,
});
console.log(`Successfully created the workspace: ${directory}.`);
}
main();

View File

@ -0,0 +1,30 @@
import {
uniq,
runCreatePackageCli,
removeTmpProject,
} from '@nx/nx-plugin/testing';
describe('<%= name %> e2e', () => {
const project = uniq('<%= name %>');
let createPackageResult;
beforeAll(async () => {
// Create project using CLI command
createPackageResult = await runCreatePackageCli(
project,
{
pluginLibraryBuildPath: '<%= pluginOutputPath %>',
createPackageLibraryBuildPath: '<%= cliOutputPath %>',
}
);
}, 240_000);
afterAll(() => {
// Remove the generated project from the file system
removeTmpProject(project);
});
it('should create project using <%= name %>', () => {
expect(createPackageResult).toContain('Successfully created');
});
});

View File

@ -0,0 +1,17 @@
import type { Linter } from '@nx/linter';
export interface CreatePackageSchema {
name: string;
project: string;
// options to create cli package, passed to js library generator
directory?: string;
skipFormat: boolean;
tags?: string;
unitTestRunner: 'jest' | 'none';
linter: Linter;
compiler: 'swc' | 'tsc';
// options to create e2e project, passed to e2e project generator
e2eTestRunner?: 'jest' | 'none';
}

View File

@ -0,0 +1,69 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxPluginCreatePackage",
"title": "Create a framework package",
"description": "Create a framework package that uses Nx CLI.",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The package name of cli, e.g. `create-framework-package`. Note this must be a valid NPM name to be published.",
"$default": {
"$source": "argv",
"index": 0
},
"x-priority": "important"
},
"project": {
"type": "string",
"description": "The name of the generator project.",
"alias": "p",
"$default": {
"$source": "projectName"
},
"x-prompt": "What is the name of the project for the generator?",
"x-priority": "important"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
},
"directory": {
"type": "string",
"description": "A directory where the app is placed."
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint"],
"default": "eslint"
},
"tags": {
"type": "string",
"description": "Add tags to the library (used for linting).",
"alias": "t"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"compiler": {
"type": "string",
"enum": ["tsc", "swc"],
"default": "tsc",
"description": "The compiler used by the build and test targets."
},
"e2eTestRunner": {
"type": "string",
"enum": ["jest", "none"],
"description": "Test runner to use for end to end (E2E) tests.",
"default": "jest"
}
},
"required": ["name", "project"]
}

View File

@ -0,0 +1,42 @@
import {
extractLayoutDirectory,
getWorkspaceLayout,
joinPathFragments,
names,
Tree,
} from '@nx/devkit';
import { CreatePackageSchema } from '../schema';
export interface NormalizedSchema extends CreatePackageSchema {
bundler: 'swc' | 'tsc';
libsDir: string;
projectName: string;
projectRoot: string;
projectDirectory: string;
}
export function normalizeSchema(
host: Tree,
schema: CreatePackageSchema
): NormalizedSchema {
const { layoutDirectory, projectDirectory } = extractLayoutDirectory(
schema.directory
);
const { libsDir: defaultLibsDir } = getWorkspaceLayout(host);
const libsDir = layoutDirectory ?? defaultLibsDir;
const name = names(schema.name).fileName;
const fullProjectDirectory = projectDirectory
? `${names(projectDirectory).fileName}/${name}`
: name;
const projectName = fullProjectDirectory.replace(new RegExp('/', 'g'), '-');
const projectRoot = joinPathFragments(libsDir, fullProjectDirectory);
return {
...schema,
bundler: schema.compiler ?? 'tsc',
libsDir,
projectName,
projectRoot,
name,
projectDirectory: fullProjectDirectory,
};
}

View File

@ -74,7 +74,7 @@ function updateWorkspaceConfiguration(host: Tree, options: NormalizedSchema) {
addProjectConfiguration(host, options.projectName, {
root: options.projectRoot,
projectType: 'application',
sourceRoot: `${options.projectRoot}/src`,
sourceRoot: `${options.projectRoot}/tests`,
targets: {
e2e: {
executor: '@nx/nx-plugin:e2e',

View File

@ -32,6 +32,6 @@ describe('<%= pluginName %> e2e', () => {
const generator = 'PLACEHOLDER';
await runNxCommandAsync(`generate <%= npmPackageName %>:${generator} --name ${name}`);
expect(() => runNxCommand('build ${proj}')).not.toThrow();
expect(() => checkFilesExist([`dist/${name}/index.js`])).not.toThrow();
expect(() => checkFilesExist(`dist/${name}/index.js`)).not.toThrow();
});
});

View File

@ -9,6 +9,7 @@ import {
writeJson,
readJson,
ExecutorsJson,
formatFiles,
} from '@nx/devkit';
import type { Tree } from '@nx/devkit';
import type { Schema } from './schema';
@ -176,6 +177,10 @@ export async function executorGenerator(host: Tree, schema: Schema) {
}
await updateExecutorJson(host, options);
if (!schema.skipFormat) {
await formatFiles(host);
}
}
export default executorGenerator;

View File

@ -5,4 +5,5 @@ export interface Schema {
unitTestRunner: 'jest' | 'none';
includeHasher: boolean;
skipLintChecks?: boolean;
skipFormat?: boolean;
}

View File

@ -51,6 +51,12 @@
"type": "boolean",
"default": false,
"description": "Do not add an eslint configuration for plugin json files."
},
"skipFormat": {
"type": "boolean",
"description": "Skip formatting files.",
"default": false,
"x-priority": "internal"
}
},
"required": ["project", "name"],

View File

@ -23,7 +23,10 @@ function normalizeOptions(tree: Tree, options: <%= className %>GeneratorSchema):
? `${names(options.directory).fileName}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const projectRoot = `${getWorkspaceLayout(tree).libsDir}/${projectDirectory}`;
const projectRoot =
getWorkspaceLayout(tree).libsDir === '.'
? '.'
: `${getWorkspaceLayout(tree).libsDir}/${projectDirectory}`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];

View File

@ -4,8 +4,6 @@ import {
joinPathFragments,
Tree,
writeJson,
} from '@nx/devkit';
import {
convertNxGenerator,
generateFiles,
getWorkspaceLayout,

View File

@ -7,6 +7,7 @@ import {
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import { PackageJson } from 'nx/src/utils/package-json';
import { pluginGenerator } from './plugin';
import { Schema } from './schema';
@ -161,7 +162,7 @@ describe('NxPlugin Plugin Generator', () => {
await pluginGenerator(tree, getSchema());
const { root } = readProjectConfiguration(tree, 'my-plugin');
const { name } = readJson<{ name: string }>(
const { name } = readJson<PackageJson>(
tree,
joinPathFragments(root, 'package.json')
);
@ -178,7 +179,7 @@ describe('NxPlugin Plugin Generator', () => {
await pluginGenerator(tree, getSchema());
const { root } = readProjectConfiguration(tree, 'my-plugin');
const { name } = readJson<{ name: string }>(
const { name } = readJson<PackageJson>(
tree,
joinPathFragments(root, 'package.json')
);
@ -193,7 +194,7 @@ describe('NxPlugin Plugin Generator', () => {
);
const { root } = readProjectConfiguration(tree, 'my-plugin');
const { name } = readJson<{ name: string }>(
const { name } = readJson<PackageJson>(
tree,
joinPathFragments(root, 'package.json')
);

View File

@ -3,9 +3,10 @@ import {
convertNxGenerator,
formatFiles,
generateFiles,
installPackagesTask,
GeneratorCallback,
normalizePath,
readProjectConfiguration,
runTasksInSerial,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
@ -73,55 +74,65 @@ function updatePluginConfig(host: Tree, options: NormalizedSchema) {
export async function pluginGenerator(host: Tree, schema: Schema) {
const options = normalizeOptions(host, schema);
const tasks: GeneratorCallback[] = [];
await jsLibraryGenerator(host, {
...schema,
config: 'project',
bundler: options.bundler,
importPath: options.npmPackageName,
skipFormat: true,
});
tasks.push(
await jsLibraryGenerator(host, {
...schema,
config: 'project',
bundler: options.bundler,
publishable: true,
importPath: options.npmPackageName,
skipFormat: true,
})
);
addTsLibDependencies(host);
tasks.push(addTsLibDependencies(host));
addDependenciesToPackageJson(
host,
{
'@nx/devkit': nxVersion,
},
{
'@nx/jest': nxVersion,
'@nx/js': nxVersion,
'@nx/nx-plugin': nxVersion,
}
tasks.push(
addDependenciesToPackageJson(
host,
{
'@nx/devkit': nxVersion,
},
{
'@nx/jest': nxVersion,
'@nx/js': nxVersion,
'@nx/nx-plugin': nxVersion,
}
)
);
// Ensures Swc Deps are installed to handle running
// local plugin generators and executors
addSwcDependencies(host);
addSwcRegisterDependencies(host);
tasks.push(addSwcDependencies(host));
tasks.push(addSwcRegisterDependencies(host));
await addFiles(host, options);
updatePluginConfig(host, options);
if (options.e2eTestRunner !== 'none') {
await e2eProjectGenerator(host, {
pluginName: options.name,
projectDirectory: options.projectDirectory,
pluginOutputPath: `dist/${options.libsDir}/${options.projectDirectory}`,
npmPackageName: options.npmPackageName,
skipFormat: true,
rootProject: options.rootProject,
});
tasks.push(
await e2eProjectGenerator(host, {
pluginName: options.name,
projectDirectory: options.projectDirectory,
pluginOutputPath: `dist/${options.libsDir}/${options.projectDirectory}`,
npmPackageName: options.npmPackageName,
skipFormat: true,
rootProject: options.rootProject,
})
);
}
if (options.linter === Linter.EsLint && !options.skipLintChecks) {
await pluginLintCheckGenerator(host, { projectName: options.name });
}
await formatFiles(host);
if (!options.skipFormat) {
await formatFiles(host);
}
return () => installPackagesTask(host);
return runTasksInSerial(...tasks);
}
export default pluginGenerator;

View File

@ -4,9 +4,9 @@ export interface Schema {
name: string;
directory?: string;
importPath?: string;
skipTsConfig: boolean;
skipFormat: boolean;
skipLintChecks: boolean;
skipTsConfig?: boolean; // default is false
skipFormat?: boolean; // default is false
skipLintChecks?: boolean; // default is false
e2eTestRunner?: 'jest' | 'none';
tags?: string;
unitTestRunner: 'jest' | 'none';

View File

@ -1,34 +1,58 @@
import { Tree, updateJson, updateNxJson, readNxJson } from '@nx/devkit';
import {
Tree,
updateJson,
updateNxJson,
readNxJson,
formatFiles,
runTasksInSerial,
GeneratorCallback,
} from '@nx/devkit';
import { Linter } from '@nx/linter';
import { PackageJson } from 'nx/src/utils/package-json';
import { pluginGenerator } from '../plugin/plugin';
import { PresetGeneratorSchema } from './schema';
import createPackageGenerator from '../create-package/create-package';
export default async function (tree: Tree, options: PresetGeneratorSchema) {
const task = await pluginGenerator(tree, {
const tasks: GeneratorCallback[] = [];
const pluginTask = await pluginGenerator(tree, {
compiler: 'tsc',
linter: Linter.EsLint,
name: options.pluginName.includes('/')
? options.pluginName.split('/')[1]
: options.pluginName,
skipFormat: false,
skipLintChecks: false,
skipTsConfig: false,
skipFormat: true,
unitTestRunner: 'jest',
importPath: options.pluginName,
rootProject: true,
e2eTestRunner: 'jest',
});
tasks.push(pluginTask);
removeNpmScope(tree);
moveNxPluginToDevDeps(tree);
return task;
if (options.createPackageName) {
const cliTask = await createPackageGenerator(tree, {
name: options.createPackageName,
project: options.pluginName,
skipFormat: true,
unitTestRunner: 'jest',
linter: Linter.EsLint,
compiler: 'tsc',
});
tasks.push(cliTask);
}
await formatFiles(tree);
return runTasksInSerial(...tasks);
}
function removeNpmScope(tree: Tree) {
updateNxJson(tree, { ...readNxJson(tree), npmScope: undefined });
}
function moveNxPluginToDevDeps(tree: Tree) {
updateJson<PackageJson>(tree, 'package.json', (json) => {
const nxPluginEntry = json.dependencies['@nx/nx-plugin'];

View File

@ -1,3 +1,4 @@
export interface PresetGeneratorSchema {
pluginName: string;
createPackageName?: string;
}

View File

@ -10,6 +10,10 @@
"type": "string",
"description": "Plugin name",
"aliases": ["name"]
},
"createPackageName": {
"type": "string",
"description": "Name of package which creates a workspace"
}
},
"required": ["pluginName"]

View File

@ -11,7 +11,7 @@ import { fileExists } from './utils';
*/
export function runCommandAsync(
command: string,
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv } = {
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = {
silenceError: false,
}
): Promise<{ stdout: string; stderr: string }> {
@ -19,7 +19,7 @@ export function runCommandAsync(
exec(
command,
{
cwd: tmpProjPath(),
cwd: opts.cwd ?? tmpProjPath(),
env: { ...process.env, ...opts.env },
},
(err, stdout, stderr) => {
@ -39,7 +39,7 @@ export function runCommandAsync(
*/
export function runNxCommandAsync(
command: string,
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv } = {
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = {
silenceError: false,
}
): Promise<{ stdout: string; stderr: string }> {

View File

@ -12,13 +12,13 @@ import { fileExists } from './utils';
*/
export function runNxCommand(
command?: string,
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv } = {
opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = {
silenceError: false,
}
): string {
function _runNxCommand(c) {
const execSyncOptions: ExecOptions = {
cwd: tmpProjPath(),
cwd: opts.cwd,
env: { ...process.env, ...opts.env },
};
if (fileExists(tmpProjPath('package.json'))) {
@ -50,11 +50,11 @@ export function runNxCommand(
export function runCommand(
command: string,
opts?: { env?: NodeJS.ProcessEnv }
opts: { env?: NodeJS.ProcessEnv; cwd?: string }
): string {
try {
return execSync(command, {
cwd: tmpProjPath(),
cwd: opts.cwd ?? tmpProjPath(),
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ...opts?.env },
}).toString();

View File

@ -0,0 +1,72 @@
import { workspaceRoot } from '@nx/devkit';
import { tmpFolder } from './paths';
import { fork } from 'child_process';
/**
* This function is used to run the create package CLI command.
* It builds the plugin library and the create package library and run the create package command with for the plugin library.
* It needs to be ran inside an Nx project. It would assume that an Nx project already exists.
* @param projectToBeCreated project name to be created using the cli
* @param pluginLibraryBuildPath e.g. dist/packages/my-plugin
* @param createPackageLibraryBuildPath e.g. dist/packages/create-my-plugin-package
* @param extraArguments extra arguments to be passed to the create package command
* @param verbose if true, NX_VERBOSE_LOGGING will be set to true
* @returns results for the create package command
*/
export function runCreatePackageCli(
projectToBeCreated: string,
{
pluginLibraryBuildPath,
createPackageLibraryBuildPath,
extraArguments,
verbose,
}: {
pluginLibraryBuildPath: string;
createPackageLibraryBuildPath: string;
extraArguments?: string[];
verbose?: boolean;
}
): Promise<string> {
return new Promise((resolve, reject) => {
const childProcess = fork(
`${workspaceRoot}/${createPackageLibraryBuildPath}/bin/index.js`,
[projectToBeCreated, ...(extraArguments || [])],
{
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
env: {
...process.env,
[`NX_E2E_PRESET_VERSION`]: `file:${workspaceRoot}/${pluginLibraryBuildPath}`,
// only add NX_VERBOSE_LOGGING if verbose is true
...(verbose && { NX_VERBOSE_LOGGING: 'true' }),
},
cwd: tmpFolder(),
}
);
// Ensure the child process is killed when the parent exits
process.on('exit', () => childProcess.kill());
process.on('SIGTERM', () => childProcess.kill());
let allMessages = '';
childProcess.on('message', (message) => {
allMessages += message;
});
childProcess.stdout.on('data', (data) => {
allMessages += data;
});
childProcess.on('error', (error) => {
reject(error);
});
childProcess.on('exit', (code) => {
if (code === 0) {
resolve(allMessages);
} else {
reject(allMessages);
}
});
});
}
export function generatedPackagePath(projectToBeCreated: string) {
return `${tmpFolder()}/${projectToBeCreated}`;
}

View File

@ -1,5 +1,6 @@
export * from './async-commands';
export * from './commands';
export * from './create-package-cli';
export * from './paths';
export * from './nx-project';
export * from './utils';

View File

@ -1,3 +1,9 @@
import { workspaceRoot } from '@nx/devkit';
export function tmpFolder() {
return `${workspaceRoot}/tmp`;
}
/**
* The directory where the e2e workspace resides in.
*
@ -6,8 +12,8 @@
*/
export function tmpProjPath(path?: string) {
return path
? `${process.cwd()}/tmp/nx-e2e/proj/${path}`
: `${process.cwd()}/tmp/nx-e2e/proj`;
? `${tmpFolder()}/nx-e2e/proj/${path}`
: `${tmpFolder()}/nx-e2e/proj`;
}
/**
@ -18,6 +24,6 @@ export function tmpProjPath(path?: string) {
*/
export function tmpBackupProjPath(path?: string) {
return path
? `${process.cwd()}/tmp/nx-e2e/proj-backup/${path}`
: `${process.cwd()}/tmp/nx-e2e/proj-backup`;
? `${workspaceRoot}/tmp/nx-e2e/proj-backup/${path}`
: `${workspaceRoot}/tmp/nx-e2e/proj-backup`;
}

View File

@ -9,7 +9,7 @@ import {
writeFileSync,
} from 'fs-extra';
import { dirname, isAbsolute } from 'path';
import { tmpProjPath } from './paths';
import { tmpFolder, tmpProjPath } from './paths';
import { parseJson } from '@nx/devkit';
import type { JsonParseOptions } from '@nx/devkit';
import { directoryExists, fileExists } from 'nx/src/utils/fileutils';
@ -135,6 +135,10 @@ export function rmDist(): void {
removeSync(`${tmpProjPath()}/dist`);
}
export function removeTmpProject(project: string): void {
removeSync(`${tmpFolder()}/${project}`);
}
/**
* Get the currend `cwd` in the process
*/

View File

@ -2,7 +2,7 @@ import { ModuleFederationConfig } from '@nx/devkit';
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
import { getModuleFederationConfig } from './utils';
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
import type { AsyncNxWebpackPlugin, NxWebpackPlugin } from '@nx/webpack';
import type { AsyncNxWebpackPlugin } from '@nx/webpack';
function determineRemoteUrl(remote: string) {
const remoteConfiguration = readCachedProjectConfiguration(remote);

View File

@ -94,5 +94,6 @@
"type": "string",
"enum": ["cypress", "jest", "detox", "none"]
}
}
},
"required": ["preset", "name"]
}