494 lines
17 KiB
TypeScript
494 lines
17 KiB
TypeScript
import { ProjectConfiguration } from '@nx/devkit';
|
|
import {
|
|
checkFilesExist,
|
|
cleanupProject,
|
|
createFile,
|
|
expectTestsPass,
|
|
getPackageManagerCommand,
|
|
newProject,
|
|
readJson,
|
|
runCLI,
|
|
runCLIAsync,
|
|
runCommand,
|
|
uniq,
|
|
updateFile,
|
|
updateJson,
|
|
} from '@nx/e2e/utils';
|
|
import type { PackageJson } from 'nx/src/utils/package-json';
|
|
|
|
import {
|
|
ASYNC_GENERATOR_EXECUTOR_CONTENTS,
|
|
NX_PLUGIN_V2_CONTENTS,
|
|
} from './nx-plugin.fixtures';
|
|
import { join } from 'path';
|
|
|
|
describe('Nx Plugin', () => {
|
|
let workspaceName: string;
|
|
|
|
beforeAll(() => {
|
|
workspaceName = newProject();
|
|
});
|
|
|
|
afterAll(() => 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/libs/${plugin}/package.json`,
|
|
`dist/libs/${plugin}/src/index.js`
|
|
);
|
|
const project = readJson(`libs/${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 --project=${plugin} --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/libs/${plugin}/src/migrations/update-${version}/update-${version}.js`,
|
|
`libs/${plugin}/src/migrations/update-${version}/update-${version}.ts`
|
|
);
|
|
const migrationsJson = readJson(`libs/${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 ${generator} --project=${plugin}`);
|
|
|
|
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(
|
|
`libs/${plugin}/src/generators/${generator}/schema.d.ts`,
|
|
`libs/${plugin}/src/generators/${generator}/schema.json`,
|
|
`libs/${plugin}/src/generators/${generator}/generator.ts`,
|
|
`libs/${plugin}/src/generators/${generator}/generator.spec.ts`,
|
|
`dist/libs/${plugin}/src/generators/${generator}/schema.d.ts`,
|
|
`dist/libs/${plugin}/src/generators/${generator}/schema.json`,
|
|
`dist/libs/${plugin}/src/generators/${generator}/generator.js`
|
|
);
|
|
const generatorJson = readJson(`libs/${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 ${executor} --project=${plugin} --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(
|
|
`libs/${plugin}/src/executors/${executor}/schema.d.ts`,
|
|
`libs/${plugin}/src/executors/${executor}/schema.json`,
|
|
`libs/${plugin}/src/executors/${executor}/executor.ts`,
|
|
`libs/${plugin}/src/executors/${executor}/hasher.ts`,
|
|
`libs/${plugin}/src/executors/${executor}/executor.spec.ts`,
|
|
`dist/libs/${plugin}/src/executors/${executor}/schema.d.ts`,
|
|
`dist/libs/${plugin}/src/executors/${executor}/schema.json`,
|
|
`dist/libs/${plugin}/src/executors/${executor}/executor.js`,
|
|
`dist/libs/${plugin}/src/executors/${executor}/hasher.js`
|
|
);
|
|
const executorsJson = readJson(`libs/${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 ${goodGenerator} --project=${plugin}`
|
|
);
|
|
|
|
runCLI(
|
|
`generate @nx/plugin:generator ${badFactoryPath} --project=${plugin}`
|
|
);
|
|
|
|
runCLI(`generate @nx/plugin:executor ${goodExecutor} --project=${plugin}`);
|
|
|
|
runCLI(
|
|
`generate @nx/plugin:executor ${badExecutorBadImplPath} --project=${plugin}`
|
|
);
|
|
|
|
runCLI(
|
|
`generate @nx/plugin:migration ${badMigrationVersion} --project=${plugin} --packageVersion="invalid"`
|
|
);
|
|
|
|
runCLI(
|
|
`generate @nx/plugin:migration ${missingMigrationVersion} --project=${plugin} --packageVersion="0.1.0"`
|
|
);
|
|
|
|
runCLI(
|
|
`generate @nx/plugin:migration ${goodMigration} --project=${plugin} --packageVersion="0.1.0"`
|
|
);
|
|
|
|
updateFile(`libs/${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(`libs/${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(`libs/${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 (v1)', async () => {
|
|
// Setup project inference + target inference
|
|
updateFile(
|
|
`libs/${plugin}/src/index.ts`,
|
|
`import {basename} from 'path'
|
|
|
|
export function registerProjectTargets(f) {
|
|
if (basename(f) === 'my-project-file') {
|
|
return {
|
|
build: {
|
|
executor: "nx:run-commands",
|
|
options: {
|
|
command: "echo 'custom registered target'"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const projectFilePatterns = ['my-project-file'];
|
|
`
|
|
);
|
|
|
|
// Register plugin in nx.json (required for inference)
|
|
updateFile(`nx.json`, (nxJson) => {
|
|
const nx = JSON.parse(nxJson);
|
|
nx.plugins = [`@${workspaceName}/${plugin}`];
|
|
return JSON.stringify(nx, null, 2);
|
|
});
|
|
|
|
// Create project that should be inferred by Nx
|
|
const inferredProject = uniq('inferred');
|
|
createFile(`libs/${inferredProject}/my-project-file`);
|
|
|
|
// Attempt to use inferred project w/ Nx
|
|
expect(runCLI(`build ${inferredProject}`)).toContain(
|
|
'custom registered target'
|
|
);
|
|
});
|
|
|
|
it('should be able to infer projects and targets (v2)', async () => {
|
|
// Setup project inference + target inference
|
|
updateFile(`libs/${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(`libs/${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']);
|
|
});
|
|
|
|
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 ${generator} --project=${plugin}`);
|
|
|
|
runCLI(`generate @nx/plugin:executor ${executor} --project=${plugin}`);
|
|
|
|
updateFile(
|
|
`libs/${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 ${generator} --project=${plugin}`
|
|
);
|
|
|
|
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 ${plugin} --linter=eslint --directory subdir --e2eTestRunner=jest`
|
|
);
|
|
checkFilesExist(`libs/subdir/${plugin}/package.json`);
|
|
const pluginProject = readJson(
|
|
join('libs', 'subdir', plugin, 'project.json')
|
|
);
|
|
const pluginE2EProject = readJson(
|
|
join('apps', '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('libs', plugin, 'project.json'));
|
|
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 @nx/plugin:plugin ${plugin} --e2eTestRunner jest --publishable`
|
|
);
|
|
runCLI(
|
|
`generate @nx/plugin:create-package ${createAppName} --project=${plugin} --e2eProject=${plugin}-e2e`
|
|
);
|
|
|
|
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`
|
|
);
|
|
|
|
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 ${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 --project-name-and-root-format=as-provided`
|
|
);
|
|
|
|
// 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} --project=${plugin} --e2eProject=${plugin}-e2e --project-name-and-root-format=as-provided`
|
|
);
|
|
|
|
// 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}`
|
|
);
|
|
});
|
|
});
|