chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
/* eslint-disable */
module.exports = {
transform: {
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
maxWorkers: 1,
globals: {},
globalSetup: '../utils/global-setup.ts',
globalTeardown: '../utils/global-teardown.ts',
displayName: 'e2e-plugin',
preset: '../jest.preset.e2e.js',
};
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@nx/e2e-plugin",
"version": "0.0.1",
"private": true,
"dependencies": {
"nx": "workspace:*",
"@nx/e2e-utils": "workspace:*"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "e2e-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/plugin",
"projectType": "application",
"implicitDependencies": ["create-nx-plugin"],
"// targets": "to see all targets run: nx show project e2e-plugin --web",
"targets": {}
}
@@ -0,0 +1,532 @@
import {
checkFilesExist,
cleanupProject,
createFile,
newProject,
readJson,
renameFile,
runCLI,
runCommand,
uniq,
updateFile,
updateJson,
} from '@nx/e2e-utils';
import {
ASYNC_GENERATOR_EXECUTOR_CONTENTS,
NX_PLUGIN_V2_CONTENTS,
} from './nx-plugin.fixtures';
describe('Nx Plugin (TS solution)', () => {
let workspaceName: string;
beforeAll(() => {
workspaceName = newProject({
preset: 'ts',
packages: ['@nx/eslint', '@nx/jest', '@nx/plugin'],
});
});
afterAll(() => cleanupProject());
it('should be able to generate a Nx Plugin with generators, executors and migrations', async () => {
const plugin = uniq('plugin');
const generator = uniq('generator');
const executor = uniq('executor');
const migrationVersion = '1.0.0';
runCLI(
`generate @nx/plugin:plugin packages/${plugin} --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest --publishable`
);
runCLI(
`generate @nx/plugin:generator packages/${plugin}/src/generators/${generator}/generator --name ${generator}`
);
runCLI(
`generate @nx/plugin:executor packages/${plugin}/src/executors/${executor}/executor --name ${executor} --includeHasher`
);
runCLI(
`generate @nx/plugin:migration packages/${plugin}/src/migrations/update-${migrationVersion}/update-${migrationVersion} --packageVersion=${migrationVersion} --packageJsonUpdates=false`
);
expect(runCLI(`lint @proj/${plugin}`)).toContain(
`Successfully ran target lint for project @proj/${plugin}`
);
expect(runCLI(`typecheck @proj/${plugin}`)).toContain(
`Successfully ran target typecheck for project @proj/${plugin}`
);
expect(runCLI(`build @proj/${plugin}`)).toContain(
`Successfully ran target build for project @proj/${plugin}`
);
checkFilesExist(
// entry point
`packages/${plugin}/dist/index.js`,
`packages/${plugin}/dist/index.d.ts`,
// generator
`packages/${plugin}/dist/generators/${generator}/schema.json`,
`packages/${plugin}/dist/generators/${generator}/schema.d.ts`,
`packages/${plugin}/dist/generators/${generator}/generator.js`,
`packages/${plugin}/dist/generators/${generator}/generator.d.ts`,
// executor
`packages/${plugin}/dist/executors/${executor}/schema.json`,
`packages/${plugin}/dist/executors/${executor}/schema.d.ts`,
`packages/${plugin}/dist/executors/${executor}/executor.js`,
`packages/${plugin}/dist/executors/${executor}/executor.d.ts`,
`packages/${plugin}/dist/executors/${executor}/hasher.js`,
`packages/${plugin}/dist/executors/${executor}/hasher.d.ts`,
// migration
`packages/${plugin}/dist/migrations/update-${migrationVersion}/update-${migrationVersion}.js`,
`packages/${plugin}/dist/migrations/update-${migrationVersion}/update-${migrationVersion}.d.ts`
);
expect(runCLI(`test @proj/${plugin}`)).toContain(
`Successfully ran target test for project @proj/${plugin}`
);
expect(runCLI(`e2e @proj/${plugin}-e2e`)).toContain(
`Successfully ran target e2e for project @proj/${plugin}-e2e`
);
// Check that inferred targets also work
updateJson('nx.json', (json) => {
json.plugins.push({
plugin: '@nx/jest/plugin',
include: ['packages/*-e2e/**/*'],
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
});
return json;
});
updateJson(`packages/${plugin}-e2e/package.json`, (json) => {
delete json.targets;
return json;
});
expect(() => runCLI(`e2e @proj/${plugin}-e2e`)).not.toThrow();
}, 90000);
it('should be able to infer projects and targets', async () => {
const plugin = uniq('plugin');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
// Setup project inference + target inference
updateFile(`packages/${plugin}/src/index.ts`, NX_PLUGIN_V2_CONTENTS);
// Register plugin in nx.json (required for inference)
updateJson(`nx.json`, (nxJson) => {
nxJson.plugins.push({
plugin: `@${workspaceName}/${plugin}`,
options: { inferredTags: ['my-tag'] },
});
return nxJson;
});
// Create project that should be inferred by Nx
const inferredProject = uniq('inferred');
createFile(
`packages/${inferredProject}/package.json`,
JSON.stringify({
name: inferredProject,
version: '0.0.1',
})
);
createFile(`packages/${inferredProject}/my-project-file`);
// Attempt to use inferred project w/ Nx
expect(runCLI(`build ${inferredProject}`)).toContain(
'custom registered target'
);
const configuration = JSON.parse(
runCLI(`show project ${inferredProject} --json`)
);
expect(configuration.tags).toContain('my-tag');
expect(configuration.metadata.technologies).toEqual(['my-plugin']);
});
it('should be able to use local generators and executors', async () => {
const plugin = uniq('plugin');
const generator = uniq('generator');
const executor = uniq('executor');
const generatedProject = uniq('project');
runCLI(`generate @nx/plugin:plugin packages/${plugin} --linter eslint`);
runCLI(
`generate @nx/plugin:generator --name ${generator} --path packages/${plugin}/src/generators/${generator}/generator`
);
runCLI(
`generate @nx/plugin:executor --name ${executor} --path packages/${plugin}/src/executors/${executor}/executor`
);
updateFile(
`packages/${plugin}/src/executors/${executor}/executor.ts`,
ASYNC_GENERATOR_EXECUTOR_CONTENTS
);
runCLI(
`generate @${workspaceName}/${plugin}:${generator} --name ${generatedProject}`
);
updateJson(`libs/${generatedProject}/project.json`, (project) => {
project.targets['execute'] = {
executor: `@${workspaceName}/${plugin}:${executor}`,
};
return project;
});
expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
expect(() => runCLI(`lint ${generatedProject}`)).not.toThrow();
});
it('should be able to resolve local generators and executors using package.json development condition export', async () => {
const plugin = uniq('plugin');
const generator = uniq('generator');
const executor = uniq('executor');
const generatedProject = uniq('project');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
// move/generate everything in the "code" folder, which is not a standard location and wouldn't
// be considered by the fall back resolution logic, so the only way it could be resolved is if
// the development condition export is used
renameFile(
`packages/${plugin}/src/index.ts`,
`packages/${plugin}/code/index.ts`
);
runCLI(
`generate @nx/plugin:generator --name ${generator} --path packages/${plugin}/code/generators/${generator}/generator`
);
runCLI(
`generate @nx/plugin:executor --name ${executor} --path packages/${plugin}/code/executors/${executor}/executor`
);
updateJson(`packages/${plugin}/package.json`, (pkg) => {
pkg.nx.sourceRoot = `packages/${plugin}/code`;
pkg.nx.targets.build.options.main = `packages/${plugin}/code/index.ts`;
pkg.nx.targets.build.options.rootDir = `packages/${plugin}/code`;
pkg.nx.targets.build.options.assets.forEach(
(asset: { input: string }) => {
asset.input = `./packages/${plugin}/code`;
}
);
pkg.exports = {
'.': {
types: './dist/index.d.ts',
development: './code/index.ts',
default: './dist/index.js',
},
'./package.json': './package.json',
'./generators.json': {
development: './generators.json',
default: './generators.json',
},
'./executors.json': './executors.json',
'./dist/generators/*/schema.json': {
development: './code/generators/*/schema.json',
default: './dist/generators/*/schema.json',
},
'./dist/generators/*/generator': {
types: './dist/generators/*/generator.d.ts',
development: './code/generators/*/generator.ts',
default: './dist/generators/*/generator.js',
},
'./dist/executors/*/schema.json': {
development: './code/executors/*/schema.json',
default: './dist/executors/*/schema.json',
},
'./dist/executors/*/executor': {
types: './dist/executors/*/executor.d.ts',
development: './code/executors/*/executor.ts',
default: './dist/executors/*/executor.js',
},
};
return pkg;
});
updateJson(`packages/${plugin}/tsconfig.lib.json`, (tsconfig) => {
tsconfig.compilerOptions.rootDir = 'code';
tsconfig.include = ['code/**/*.ts'];
return tsconfig;
});
updateFile(
`packages/${plugin}/code/executors/${executor}/executor.ts`,
ASYNC_GENERATOR_EXECUTOR_CONTENTS
);
runCLI(
`generate @${workspaceName}/${plugin}:${generator} --name ${generatedProject}`
);
updateJson(`libs/${generatedProject}/project.json`, (project) => {
project.targets['execute'] = {
executor: `@${workspaceName}/${plugin}:${executor}`,
};
return project;
});
expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
});
it('should load local plugins registered via subpath imports', async () => {
const plugin = uniq('plugin');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
const sourceCondition =
readJson('tsconfig.base.json').compilerOptions.customConditions[0];
expect(sourceCondition).not.toBe('development');
// expose a subpath plugin from a non-default location to ensure resolution
// goes through the workspace custom condition rather than any heuristic
updateFile(
`packages/${plugin}/src/plugins/cypress/plugin.ts`,
NX_PLUGIN_V2_CONTENTS
);
updateJson(`packages/${plugin}/package.json`, (pkg) => {
pkg.exports = {
...pkg.exports,
'./cypress': {
[sourceCondition]: './src/plugins/cypress/plugin.ts',
default: './dist/plugins/cypress/plugin.js',
},
};
return pkg;
});
updateJson(`nx.json`, (nxJson) => {
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: `@${workspaceName}/${plugin}/cypress`,
options: { inferredTags: ['cypress-tag'] },
});
return nxJson;
});
const inferredProject = uniq('subpath-inferred');
createFile(
`packages/${inferredProject}/package.json`,
JSON.stringify({ name: inferredProject, version: '0.0.1' })
);
createFile(`packages/${inferredProject}/my-project-file`);
expect(runCLI(`build ${inferredProject}`)).toContain(
'custom registered target'
);
const configuration = JSON.parse(
runCLI(`show project ${inferredProject} --json`)
);
expect(configuration.tags).toContain('cypress-tag');
});
// Regression: a subpath plugin whose TS source uses NodeNext `.js` relative
// imports (TS resolves them to the sibling `.ts` at compile time) needs
// `.js -> .ts` rewriting on whichever resolver layer applies:
// - type: module -> loads as ESM; native type stripping loads the `.ts`
// and Nx's self-contained ESM resolve hook rewrites the specifier (no
// ts-node/swc-node required).
// - type: commonjs -> native strip can't run ESM `import` syntax in a CJS
// module, so Nx falls back to swc/ts-node to transpile, then the
// `Module._resolveFilename` patch rewrites the emitted `require('./x.js')`.
// Both shapes must load correctly.
for (const moduleType of ['module', 'commonjs'] as const) {
it(`should load local plugin subpath imports whose TS sources use NodeNext-style .js import specifiers (type: ${moduleType})`, async () => {
const plugin = uniq('plugin');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
const sourceCondition =
readJson('tsconfig.base.json').compilerOptions.customConditions[0];
expect(sourceCondition).not.toBe('development');
// Plugin entry imports a sibling helper via NodeNext `.js` specifier
// (which TS resolves to the sibling `.ts` file at compile time).
updateFile(
`packages/${plugin}/src/plugins/docker/index.ts`,
`import { dockerCreateNodes, dockerCreateMetadata } from './nodes.js';
import type { CreateNodesV2, CreateMetadata } from '@nx/devkit';
type PluginOptions = { inferredTags: string[] };
export const createNodesV2: CreateNodesV2<PluginOptions> = [
'**/my-project-file',
dockerCreateNodes,
];
export const createMetadata: CreateMetadata = dockerCreateMetadata;
`
);
updateFile(
`packages/${plugin}/src/plugins/docker/nodes.ts`,
`import { basename, dirname } from 'path';
import type { CreateMetadata, ProjectsMetadata } from '@nx/devkit';
type PluginOptions = { inferredTags: string[] };
export const dockerCreateMetadata: CreateMetadata = (graph) => {
const metadata: ProjectsMetadata = {};
for (const projectNode of Object.values(graph.nodes)) {
metadata[projectNode.name] = {
metadata: { technologies: ['my-plugin'] },
};
}
return metadata;
};
export const dockerCreateNodes = (files: string[], options: PluginOptions) => {
const results: any[] = [];
for (const f of files) {
const root = dirname(f);
const name = basename(root);
results.push([
f,
{
projects: {
[root]: {
root,
name,
targets: {
build: {
executor: 'nx:run-commands',
options: { command: "echo 'custom registered target'" },
},
},
tags: options.inferredTags,
},
},
},
]);
}
return results;
};
`
);
updateJson(`packages/${plugin}/package.json`, (pkg) => {
pkg.type = moduleType;
pkg.exports = {
...pkg.exports,
'./docker': {
[sourceCondition]: './src/plugins/docker/index.ts',
types: './dist/plugins/docker/index.d.ts',
import: './dist/plugins/docker/index.js',
default: './dist/plugins/docker/index.js',
},
};
return pkg;
});
updateJson(`nx.json`, (nxJson) => {
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: `@${workspaceName}/${plugin}/docker`,
options: { inferredTags: ['docker-tag'] },
});
return nxJson;
});
const inferredProject = uniq('docker-inferred');
createFile(
`packages/${inferredProject}/package.json`,
JSON.stringify({ name: inferredProject, version: '0.0.1' })
);
createFile(`packages/${inferredProject}/my-project-file`);
expect(runCLI(`build ${inferredProject}`)).toContain(
'custom registered target'
);
const configuration = JSON.parse(
runCLI(`show project ${inferredProject} --json`)
);
expect(configuration.tags).toContain('docker-tag');
});
}
it('should load local plugin subpath imports from dist when no source condition is declared', async () => {
const plugin = uniq('plugin');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
// Expose a subpath plugin via the dist artifact only (no custom source condition).
// Nx should fall through to whatever resolve.exports returns — the dist file —
// rather than hard-failing.
createFile(
`packages/${plugin}/dist/plugins/cypress/plugin.js`,
`const { basename, dirname } = require("path");
exports.createNodesV2 = [
"**/my-project-file",
(files, options) =>
files.map((f) => {
const root = dirname(f);
const name = basename(root);
return [
f,
{
projects: {
[root]: {
root,
name,
targets: {
build: {
executor: "nx:run-commands",
options: {
command: "echo 'dist registered target'",
},
},
},
tags: options.inferredTags,
},
},
},
];
}),
];
`
);
updateJson(`packages/${plugin}/package.json`, (pkg) => {
pkg.exports = {
...pkg.exports,
'./cypress': {
default: './dist/plugins/cypress/plugin.js',
},
};
return pkg;
});
updateJson(`nx.json`, (nxJson) => {
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: `@${workspaceName}/${plugin}/cypress`,
options: { inferredTags: ['cypress-tag'] },
});
return nxJson;
});
const inferredProject = uniq('subpath-inferred');
createFile(
`packages/${inferredProject}/package.json`,
JSON.stringify({ name: inferredProject, version: '0.0.1' })
);
createFile(`packages/${inferredProject}/my-project-file`);
// With no source condition, Nx resolves to the dist artifact — loading
// should succeed (not hard-fail with a "custom condition" error).
expect(runCLI(`build ${inferredProject}`)).toContain(
'dist registered target'
);
});
it('should respect and support generating plugins with a name different than the import path', async () => {
const plugin = uniq('plugin');
runCLI(
`generate @nx/plugin:plugin packages/${plugin} --name=${plugin} --linter=eslint --publishable`
);
const packageJson = readJson(`packages/${plugin}/package.json`);
expect(packageJson.nx.name).toBe(plugin);
expect(runCLI(`build ${plugin}`)).toContain(
`Successfully ran target build for project ${plugin}`
);
expect(runCLI(`lint ${plugin}`)).toContain(
`Successfully ran target lint for project ${plugin}`
);
}, 90000);
});
+77
View File
@@ -0,0 +1,77 @@
export const ASYNC_GENERATOR_EXECUTOR_CONTENTS = `import type { ExecutorContext } from '@nx/devkit';
async function* asyncGenerator(
) {
for (let i = 5; i < 10; i++) {
yield new Promise((res) => setTimeout(() => res({ success: true }), 5));
}
yield { success: true };
}
export default async function* execute(
options: unknown,
context: ExecutorContext
) {
for (let i = 5; i < 10; i++) {
yield new Promise((res) => setTimeout(() => res({ success: true }), 5));
}
yield* asyncGenerator();
}
`;
export const NX_PLUGIN_V2_CONTENTS = `import { basename, dirname } from "path";
import type { CreateNodesV2, CreateMetadata, ProjectsMetadata } from "@nx/devkit";
type PluginOptions = {
inferredTags: string[]
}
export const createMetadata: CreateMetadata = (graph) => {
const metadata: ProjectsMetadata = {};
for (const projectNode of Object.values(graph.nodes)) {
metadata[projectNode.name] = {
metadata: {
technologies: ["my-plugin"]
}
}
}
return metadata;
}
export const createNodesV2: CreateNodesV2<PluginOptions> = [
"**/my-project-file",
(files, options, ctx) => {
const results = [];
for (const f of files) {
// f = path/to/my/file/my-project-file
const root = dirname(f);
// root = path/to/my/file
const name = basename(root);
// name = file
results.push([
f,
{
projects: {
[root]: {
root,
name,
targets: {
build: {
executor: "nx:run-commands",
options: {
command: "echo 'custom registered target'",
},
},
},
tags: options.inferredTags,
},
},
},
]);
}
return results;
},
];
`;
+513
View File
@@ -0,0 +1,513 @@
import { ProjectConfiguration } from '@nx/devkit';
import {
checkFilesExist,
cleanupProject,
createFile,
expectTestsPass,
getPackageManagerCommand,
newProject,
readJson,
runCLI,
runCLIAsync,
runCommand,
tmpProjPath,
trimDaemonLog,
uniq,
updateFile,
updateJson,
} from '@nx/e2e-utils';
import type { PackageJson } from 'nx/src/utils/package-json';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'path';
import {
ASYNC_GENERATOR_EXECUTOR_CONTENTS,
NX_PLUGIN_V2_CONTENTS,
} from './nx-plugin.fixtures';
describe('Nx Plugin', () => {
let workspaceName: string;
beforeAll(() => {
workspaceName = newProject({
packages: ['@nx/eslint', '@nx/jest', '@nx/plugin'],
});
});
afterAll(() => {
// The suite shares one long-lived daemon (no `reset`), so dump its log
// once before teardown — CI shows it for a daemon crash on plugin load.
try {
const daemonLog = join(
tmpProjPath(),
'.nx',
'workspace-data',
'd',
'daemon.log'
);
if (existsSync(daemonLog)) {
// Trimmed — see trimDaemonLog; the raw log is thousands of lines.
console.log(
`\n========== daemon.log (trimmed) ==========\n${trimDaemonLog(
readFileSync(daemonLog, 'utf-8')
)}\n========== end daemon.log ==========\n`
);
} else {
console.log(`[plugin-debug] no daemon log at ${daemonLog}`);
}
} catch (e) {
console.log(`[plugin-debug] failed to read daemon log: ${e}`);
}
cleanupProject();
});
it('should be able to generate a Nx Plugin ', async () => {
const plugin = uniq('plugin');
runCLI(
`generate @nx/plugin:plugin ${plugin} --linter=eslint --e2eTestRunner=jest --publishable`
);
const lintResults = runCLI(`lint ${plugin}`);
expect(lintResults).toContain('All files pass linting');
const buildResults = runCLI(`build ${plugin}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`dist/${plugin}/package.json`,
`dist/${plugin}/src/index.js`
);
const project = readJson(`${plugin}/project.json`);
expect(project).toMatchObject({
tags: [],
});
runCLI(`e2e ${plugin}-e2e`);
}, 90000);
it('should be able to generate a migration', async () => {
const plugin = uniq('plugin');
const version = '1.0.0';
runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`);
runCLI(
`generate @nx/plugin:migration --path=${plugin}/src/migrations/update-${version}/update-${version} --packageVersion=${version} --packageJsonUpdates=false`
);
const lintResults = runCLI(`lint ${plugin}`);
expect(lintResults).toContain('All files pass linting');
expectTestsPass(await runCLIAsync(`test ${plugin}`));
const buildResults = runCLI(`build ${plugin}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`dist/${plugin}/src/migrations/update-${version}/update-${version}.js`,
`${plugin}/src/migrations/update-${version}/update-${version}.ts`
);
const migrationsJson = readJson(`${plugin}/migrations.json`);
expect(migrationsJson).toMatchObject({
generators: expect.objectContaining({
[`update-${version}`]: {
version,
description: `Migration for v1.0.0`,
implementation: `./src/migrations/update-${version}/update-${version}`,
},
}),
});
}, 90000);
it('should be able to generate a generator', async () => {
const plugin = uniq('plugin');
const generator = uniq('generator');
runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`);
runCLI(
`generate @nx/plugin:generator ${plugin}/src/generators/${generator}/generator --name ${generator}`
);
const lintResults = runCLI(`lint ${plugin}`);
expect(lintResults).toContain('All files pass linting');
expectTestsPass(await runCLIAsync(`test ${plugin}`));
const buildResults = runCLI(`build ${plugin}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`${plugin}/src/generators/${generator}/schema.d.ts`,
`${plugin}/src/generators/${generator}/schema.json`,
`${plugin}/src/generators/${generator}/generator.ts`,
`${plugin}/src/generators/${generator}/generator.spec.ts`,
`dist/${plugin}/src/generators/${generator}/schema.d.ts`,
`dist/${plugin}/src/generators/${generator}/schema.json`,
`dist/${plugin}/src/generators/${generator}/generator.js`
);
const generatorJson = readJson(`${plugin}/generators.json`);
expect(generatorJson).toMatchObject({
generators: expect.objectContaining({
[generator]: {
factory: `./src/generators/${generator}/generator`,
schema: `./src/generators/${generator}/schema.json`,
description: `${generator} generator`,
},
}),
});
}, 90000);
it('should be able to generate an executor', async () => {
const plugin = uniq('plugin');
const executor = uniq('executor');
runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`);
runCLI(
`generate @nx/plugin:executor --name ${executor} --path=${plugin}/src/executors/${executor}/executor --includeHasher`
);
const lintResults = runCLI(`lint ${plugin}`);
expect(lintResults).toContain('All files pass linting');
expectTestsPass(await runCLIAsync(`test ${plugin}`));
const buildResults = runCLI(`build ${plugin}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`${plugin}/src/executors/${executor}/schema.d.ts`,
`${plugin}/src/executors/${executor}/schema.json`,
`${plugin}/src/executors/${executor}/executor.ts`,
`${plugin}/src/executors/${executor}/hasher.ts`,
`${plugin}/src/executors/${executor}/executor.spec.ts`,
`dist/${plugin}/src/executors/${executor}/schema.d.ts`,
`dist/${plugin}/src/executors/${executor}/schema.json`,
`dist/${plugin}/src/executors/${executor}/executor.js`,
`dist/${plugin}/src/executors/${executor}/hasher.js`
);
const executorsJson = readJson(`${plugin}/executors.json`);
expect(executorsJson).toMatchObject({
executors: expect.objectContaining({
[executor]: {
implementation: `./src/executors/${executor}/executor`,
hasher: `./src/executors/${executor}/hasher`,
schema: `./src/executors/${executor}/schema.json`,
description: `${executor} executor`,
},
}),
});
}, 90000);
it('should catch invalid implementations, schemas, and version in lint', async () => {
const plugin = uniq('plugin');
const goodGenerator = uniq('good-generator');
const goodExecutor = uniq('good-executor');
const badExecutorBadImplPath = uniq('bad-executor');
const goodMigration = uniq('good-migration');
const badFactoryPath = uniq('bad-generator');
const badMigrationVersion = uniq('bad-version');
const missingMigrationVersion = uniq('missing-version');
// Generating the plugin results in a generator also called {plugin},
// as well as an executor called "build"
runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`);
runCLI(
`generate @nx/plugin:generator --name=${goodGenerator} --path=${plugin}/src/generators/${goodGenerator}/generator`
);
runCLI(
`generate @nx/plugin:generator --name=${badFactoryPath} --path=${plugin}/src/generators/${badFactoryPath}/generator`
);
runCLI(
`generate @nx/plugin:executor --name=${goodExecutor} --path=${plugin}/src/executors/${goodExecutor}/executor`
);
runCLI(
`generate @nx/plugin:executor --name=${badExecutorBadImplPath} --path=${plugin}/src/executors/${badExecutorBadImplPath}/executor`
);
runCLI(
`generate @nx/plugin:migration --name=${badMigrationVersion} --path=${plugin}/src/migrations --packageVersion="invalid"`
);
runCLI(
`generate @nx/plugin:migration --name=${missingMigrationVersion} --path=${plugin}/migrations/0.1.0 --packageVersion="0.1.0"`
);
runCLI(
`generate @nx/plugin:migration --name=${goodMigration} --path=${plugin}/migrations/0.1.0 --packageVersion="0.1.0"`
);
updateFile(`${plugin}/generators.json`, (f) => {
const json = JSON.parse(f);
// @proj/plugin:plugin has an invalid implementation path
json.generators[badFactoryPath].factory =
`./generators/${plugin}/bad-path`;
// @proj/plugin:non-existant has a missing implementation path amd schema
json.generators['non-existant-generator'] = {};
return JSON.stringify(json);
});
updateFile(`${plugin}/executors.json`, (f) => {
const json = JSON.parse(f);
// @proj/plugin:badExecutorBadImplPath has an invalid implementation path
json.executors[badExecutorBadImplPath].implementation =
'./executors/bad-path';
// @proj/plugin:non-existant has a missing implementation path amd schema
json.executors['non-existant-executor'] = {};
return JSON.stringify(json);
});
updateFile(`${plugin}/migrations.json`, (f) => {
const json = JSON.parse(f);
delete json.generators[missingMigrationVersion].version;
return JSON.stringify(json);
});
const results = runCLI(`lint ${plugin}`, { silenceError: true });
expect(results).toContain(
`${badFactoryPath}: Implementation path should point to a valid file`
);
expect(results).toContain(
`non-existant-generator: Missing required property - \`schema\``
);
expect(results).toContain(
`non-existant-generator: Missing required property - \`implementation\``
);
expect(results).not.toContain(goodGenerator);
expect(results).toContain(
`${badExecutorBadImplPath}: Implementation path should point to a valid file`
);
expect(results).toContain(
`non-existant-executor: Missing required property - \`schema\``
);
expect(results).toContain(
`non-existant-executor: Missing required property - \`implementation\``
);
expect(results).not.toContain(goodExecutor);
expect(results).toContain(
`${missingMigrationVersion}: Missing required property - \`version\``
);
expect(results).toContain(
`${badMigrationVersion}: Version should be a valid semver`
);
expect(results).not.toContain(goodMigration);
});
describe('local plugins', () => {
let plugin: string;
beforeEach(() => {
plugin = uniq('plugin');
runCLI(`generate @nx/plugin:plugin ${plugin} --linter=eslint`);
});
it('should be able to infer projects and targets', async () => {
// Setup project inference + target inference
updateFile(`${plugin}/src/index.ts`, NX_PLUGIN_V2_CONTENTS);
// Register plugin in nx.json (required for inference)
updateFile(`nx.json`, (nxJson) => {
const nx = JSON.parse(nxJson);
nx.plugins = [
{
plugin: `@${workspaceName}/${plugin}`,
options: { inferredTags: ['my-tag'] },
},
];
return JSON.stringify(nx, null, 2);
});
// Create project that should be inferred by Nx
const inferredProject = uniq('inferred');
createFile(`${inferredProject}/my-project-file`);
// Attempt to use inferred project w/ Nx
expect(runCLI(`build ${inferredProject}`)).toContain(
'custom registered target'
);
const configuration = JSON.parse(
runCLI(`show project ${inferredProject} --json`)
);
expect(configuration.tags).toEqual(['my-tag']);
expect(configuration.metadata.technologies).toEqual(['my-plugin']);
});
it('should be able to use local generators and executors', async () => {
const generator = uniq('generator');
const executor = uniq('executor');
const generatedProject = uniq('project');
runCLI(
`generate @nx/plugin:generator --name ${generator} --path ${plugin}/src/generators/${generator}/generator`
);
runCLI(
`generate @nx/plugin:executor --name ${executor} --path ${plugin}/src/executors/${executor}/executor`
);
updateFile(
`${plugin}/src/executors/${executor}/executor.ts`,
ASYNC_GENERATOR_EXECUTOR_CONTENTS
);
runCLI(
`generate @${workspaceName}/${plugin}:${generator} --name ${generatedProject}`
);
updateFile(`libs/${generatedProject}/project.json`, (f) => {
const project: ProjectConfiguration = JSON.parse(f);
project.targets['execute'] = {
executor: `@${workspaceName}/${plugin}:${executor}`,
};
return JSON.stringify(project, null, 2);
});
expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
});
it('should work with ts-node only', async () => {
const oldPackageJson: PackageJson = readJson('package.json');
updateJson<PackageJson>('package.json', (j) => {
delete j.dependencies['@swc-node/register'];
delete j.devDependencies['@swc-node/register'];
return j;
});
runCommand(getPackageManagerCommand().install);
const generator = uniq('generator');
expect(() => {
runCLI(
`generate @nx/plugin:generator ${plugin}/src/generators/${generator}/generator --name ${generator}`
);
runCLI(
`generate @${workspaceName}/${plugin}:${generator} --name ${uniq(
'test'
)}`
);
}).not.toThrow();
updateFile('package.json', JSON.stringify(oldPackageJson, null, 2));
runCommand(getPackageManagerCommand().install);
});
});
describe('--directory', () => {
it('should create a plugin in the specified directory', async () => {
const plugin = uniq('plugin');
runCLI(
`generate @nx/plugin:plugin libs/subdir/${plugin} --linter=eslint --e2eTestRunner=jest`
);
checkFilesExist(`libs/subdir/${plugin}/package.json`);
const pluginProject = readJson(
join('libs', 'subdir', plugin, 'project.json')
);
const pluginE2EProject = readJson(
join('libs', 'subdir', `${plugin}-e2e`, 'project.json')
);
expect(pluginProject.targets).toBeDefined();
expect(pluginE2EProject).toBeTruthy();
}, 90000);
});
describe('--tags', () => {
it('should add tags to project configuration', () => {
const plugin = uniq('plugin');
runCLI(
`generate @nx/plugin:plugin ${plugin} --linter=eslint --tags=e2etag,e2ePackage `
);
const pluginProject = readJson(join(plugin, 'project.json'));
expect(pluginProject.tags).toEqual(['e2etag', 'e2ePackage']);
}, 90000);
});
it('should be able to generate a create-package plugin without e2e tests', async () => {
const plugin = uniq('plugin');
const createAppName = `create-${plugin}-app`;
runCLI(
`generate @nx/plugin:plugin ${plugin} --e2eTestRunner jest --publishable`
);
runCLI(
`generate @nx/plugin:create-package ${createAppName} --name=${createAppName} --project=${plugin} --verbose`
);
const buildResults = runCLI(`build ${createAppName}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`${plugin}/src/generators/preset`,
`${createAppName}`,
`dist/${createAppName}/bin/index.js`
);
});
it('should be able to generate a create-package plugin ', async () => {
const plugin = uniq('plugin');
const createAppName = `create-${plugin}-app`;
runCLI(
`generate @nx/plugin:plugin ${plugin} --e2eTestRunner jest --publishable`
);
runCLI(
`generate @nx/plugin:create-package ${createAppName} --name=${createAppName} --project=${plugin} --e2eProject=${plugin}-e2e --verbose`
);
const buildResults = runCLI(`build ${createAppName}`);
expect(buildResults).toContain('Done compiling TypeScript files');
checkFilesExist(
`${plugin}/src/generators/preset`,
`${createAppName}`,
`dist/${createAppName}/bin/index.js`
);
runCLI(`e2e ${plugin}-e2e`);
});
it('should throw an error when run create-package for an invalid plugin ', async () => {
const plugin = uniq('plugin');
expect(() =>
runCLI(
`generate @nx/plugin:create-package create-${plugin} --name=create-${plugin} --project=invalid-plugin`
)
).toThrow();
});
it('should support the new name and root format', async () => {
const plugin = uniq('plugin');
const createAppName = `create-${plugin}-app`;
runCLI(
`generate @nx/plugin:plugin ${plugin} --e2eTestRunner jest --publishable`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${plugin}/src/index.ts`);
// check build works
expect(runCLI(`build ${plugin}`)).toContain(
`Successfully ran target build for project ${plugin}`
);
// check tests pass
const appTestResult = runCLI(`test ${plugin}`);
expect(appTestResult).toContain(
`Successfully ran target test for project ${plugin}`
);
runCLI(
`generate @nx/plugin:create-package ${createAppName} --name=${createAppName} --project=${plugin} --e2eProject=${plugin}-e2e`
);
// check files are generated without the layout directory ("libs/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${plugin}/src/generators/preset`, `${createAppName}`);
// check build works
expect(runCLI(`build ${createAppName}`)).toContain(
`Successfully ran target build for project ${createAppName}`
);
// check tests pass
const libTestResult = runCLI(`test ${createAppName}`);
expect(libTestResult).toContain(
`Successfully ran target test for project ${createAppName}`
);
});
});
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "../utils"
},
{
"path": "../../packages/nx"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "out-tsc/spec",
"types": ["jest", "node"]
},
"exclude": ["out-tsc"],
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"src/**/*.ts",
"**/*.d.ts",
"jest.config.ts"
]
}