feat(nx-plugin): add verdaccio to create package e2e (#17566)

This commit is contained in:
Emily Xiong 2023-06-16 09:45:52 -04:00 committed by GitHub
parent 93b123a326
commit 7baad04ea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 244 additions and 186 deletions

View File

@ -57,11 +57,12 @@
"default": "tsc", "default": "tsc",
"description": "The compiler used by the build and test targets." "description": "The compiler used by the build and test targets."
}, },
"e2eTestRunner": { "e2eProject": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "description": "The name of the e2e project.",
"description": "Test runner to use for end to end (E2E) tests.", "alias": "p",
"default": "jest" "$default": { "$source": "projectName" },
"x-prompt": "What is the name of the e2e project?"
} }
}, },
"required": ["name", "project"], "required": ["name", "project"],

View File

@ -22,7 +22,7 @@ export default async function (globalConfig: Config.ConfigGlobals) {
{ stdio: 'pipe' } { stdio: 'pipe' }
); );
childProcess?.stdout?.on('data', (data) => { const listener = (data) => {
if (data.toString().includes('http://localhost:')) { if (data.toString().includes('http://localhost:')) {
const port = parseInt( const port = parseInt(
data.toString().match(/localhost:(?<port>\d+)/)?.groups?.port data.toString().match(/localhost:(?<port>\d+)/)?.groups?.port
@ -35,11 +35,12 @@ export default async function (globalConfig: Config.ConfigGlobals) {
console.log('Set npm and yarn config registry to ' + registry); console.log('Set npm and yarn config registry to ' + registry);
resolve(childProcess); resolve(childProcess);
childProcess.stdout?.off('data', listener);
} }
}); };
childProcess?.stdout?.on('data', listener);
childProcess?.stderr?.on('data', (data) => { childProcess?.stderr?.on('data', (data) => {
process.stderr.write(data); process.stderr.write(data);
reject(data);
}); });
childProcess.on('error', (err) => { childProcess.on('error', (err) => {
console.log('local registry error', err); console.log('local registry error', err);

View File

@ -6,7 +6,6 @@ import {
uniq, uniq,
runCreatePlugin, runCreatePlugin,
cleanupProject, cleanupProject,
tmpProjPath,
} from '@nx/e2e/utils'; } from '@nx/e2e/utils';
describe('create-nx-plugin', () => { describe('create-nx-plugin', () => {
@ -64,7 +63,8 @@ describe('create-nx-plugin', () => {
runCLI(`build ${pluginName}`); runCLI(`build ${pluginName}`);
checkFilesExist( checkFilesExist(
`dist/${pluginName}/package.json`, `dist/${pluginName}/package.json`,
`dist/${pluginName}/generators.json` `dist/${pluginName}/generators.json`,
`e2e/tests/${pluginName}.spec.ts`
); );
runCLI(`build create-${pluginName}-package`); runCLI(`build create-${pluginName}-package`);

View File

@ -0,0 +1 @@
export * from '../../src/plugins/jest/start-local-registry';

View File

@ -1,8 +1,8 @@
import { ExecutorContext, logger } from '@nx/devkit'; import { ExecutorContext, logger } from '@nx/devkit';
import { removeSync, existsSync } from 'fs-extra'; import { existsSync, rmSync } from 'fs-extra';
import { ChildProcess, execSync, fork } from 'child_process'; import { ChildProcess, execSync, fork } from 'child_process';
import * as detectPort from 'detect-port'; import * as detectPort from 'detect-port';
import { join } from 'path'; import { join, resolve } from 'path';
import { VerdaccioExecutorSchema } from './schema'; import { VerdaccioExecutorSchema } from './schema';
@ -25,8 +25,12 @@ export async function verdaccioExecutor(
); );
} }
if (options.clear && options.storage && existsSync(options.storage)) { if (options.storage) {
removeSync(options.storage); options.storage = resolve(context.root, options.storage);
if (options.clear && existsSync(options.storage)) {
rmSync(options.storage, { recursive: true, force: true });
console.log(`Cleared local registry storage folder ${options.storage}`);
}
} }
const cleanupFunctions = const cleanupFunctions =
@ -84,23 +88,10 @@ function startVerdaccio(
? { VERDACCIO_STORAGE_PATH: options.storage } ? { VERDACCIO_STORAGE_PATH: options.storage }
: {}), : {}),
}, },
stdio: ['inherit', 'pipe', 'pipe', 'ipc'], stdio: 'inherit',
} }
); );
childProcess.stdout.on('data', (data) => {
process.stdout.write(data);
});
childProcess.stderr.on('data', (data) => {
if (
data.includes('VerdaccioWarning') ||
data.includes('DeprecationWarning')
) {
process.stdout.write(data);
} else {
reject(data);
}
});
childProcess.on('error', (err) => { childProcess.on('error', (err) => {
reject(err); reject(err);
}); });

View File

@ -8,7 +8,7 @@ auth:
# a list of other known repositories we can talk to # a list of other known repositories we can talk to
uplinks: uplinks:
npmjs: npmjs:
url: https://registry.npmjs.org/ url: <%= npmUplinkRegistry %>
maxage: 60m maxage: 60m
packages: packages:

View File

@ -13,13 +13,18 @@ import {
import * as path from 'path'; import * as path from 'path';
import { SetupVerdaccioGeneratorSchema } from './schema'; import { SetupVerdaccioGeneratorSchema } from './schema';
import { verdaccioVersion } from '../../utils/versions'; import { verdaccioVersion } from '../../utils/versions';
import { execSync } from 'child_process';
export async function setupVerdaccio( export async function setupVerdaccio(
tree: Tree, tree: Tree,
options: SetupVerdaccioGeneratorSchema options: SetupVerdaccioGeneratorSchema
) { ) {
if (!tree.exists('.verdaccio/config.yml')) { if (!tree.exists('.verdaccio/config.yml')) {
generateFiles(tree, path.join(__dirname, 'files'), '.verdaccio', {}); generateFiles(tree, path.join(__dirname, 'files'), '.verdaccio', {
npmUplinkRegistry:
execSync('npm config get registry')?.toString()?.trim() ??
'https://registry.npmjs.org',
});
} }
const verdaccioTarget: TargetConfiguration = { const verdaccioTarget: TargetConfiguration = {

View File

@ -0,0 +1,75 @@
import { execSync, fork } from 'child_process';
/**
* This function is used to start a local registry for testing purposes.
* @param localRegistryTarget the target to run to start the local registry e.g. workspace:local-registry
* @param storage the storage location for the local registry
* @param verbose whether to log verbose output
*/
export function startLocalRegistry({
localRegistryTarget,
storage,
verbose,
}: {
localRegistryTarget: string;
storage?: string;
verbose?: boolean;
}) {
if (!localRegistryTarget) {
throw new Error(`localRegistryTarget is required`);
}
return new Promise<() => void>((resolve, reject) => {
const childProcess = fork(
require.resolve('nx'),
[
...`run ${localRegistryTarget} --location none --clear true`.split(' '),
...(storage ? [`--storage`, storage] : []),
],
{ stdio: 'pipe' }
);
const listener = (data) => {
if (verbose) {
process.stdout.write(data);
}
if (data.toString().includes('http://localhost:')) {
const port = parseInt(
data.toString().match(/localhost:(?<port>\d+)/)?.groups?.port
);
console.log('Local registry started on port ' + port);
const registry = `http://localhost:${port}`;
process.env.npm_config_registry = registry;
process.env.YARN_REGISTRY = registry;
execSync(
`npm config set //localhost:${port}/:_authToken "secretVerdaccioToken"`
);
console.log('Set npm and yarn config registry to ' + registry);
resolve(() => {
childProcess.kill();
execSync(`npm config delete //localhost:${port}/:_authToken`);
});
childProcess?.stdout?.off('data', listener);
}
};
childProcess?.stdout?.on('data', listener);
childProcess?.stderr?.on('data', (data) => {
process.stderr.write(data);
});
childProcess.on('error', (err) => {
console.log('local registry error', err);
reject(err);
});
childProcess.on('exit', (code) => {
console.log('local registry exit', code);
if (code !== 0) {
reject(code);
} else {
resolve(() => {});
}
});
});
}
export default startLocalRegistry;

View File

@ -0,0 +1,64 @@
import { ProjectConfiguration, readJson, type Tree } from '@nx/devkit';
const startLocalRegistryScript = (localRegistryTarget: string) => `
/**
* This script starts a local registry for e2e testing purposes.
* It is meant to be called in jest's globalSetup.
*/
import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry';
import { execFileSync } from 'child_process';
export default async () => {
// local registry target to run
const localRegistryTarget = '${localRegistryTarget}';
// storage folder for the local registry
const storage = './tmp/local-registry/storage';
global.stopLocalRegistry = await startLocalRegistry({
localRegistryTarget,
storage,
verbose: false,
});
const nx = require.resolve('nx');
execFileSync(
nx,
['run-many', '--targets', 'publish', '--ver', '1.0.0', '--tag', 'e2e'],
{ env: process.env, stdio: 'inherit' }
);
};
`;
const stopLocalRegistryScript = `
/**
* This script stops the local registry for e2e testing purposes.
* It is meant to be called in jest's globalTeardown.
*/
export default () => {
if (global.stopLocalRegistry) {
global.stopLocalRegistry();
}
};
`;
export function addLocalRegistryScripts(tree: Tree) {
const startLocalRegistryPath = 'tools/scripts/start-local-registry.ts';
const stopLocalRegistryPath = 'tools/scripts/stop-local-registry.ts';
const projectConfiguration: ProjectConfiguration = readJson(
tree,
'project.json'
);
const localRegistryTarget = `${projectConfiguration.name}:local-registry`;
if (!tree.exists(startLocalRegistryPath)) {
tree.write(
startLocalRegistryPath,
startLocalRegistryScript(localRegistryTarget)
);
}
if (!tree.exists(stopLocalRegistryPath)) {
tree.write(stopLocalRegistryPath, stopLocalRegistryScript);
}
return { startLocalRegistryPath, stopLocalRegistryPath };
}

View File

@ -12,14 +12,13 @@ const publishScriptContent = `
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs'; import { readFileSync, writeFileSync } from 'fs';
import chalk from 'chalk';
import devkit from '@nx/devkit'; import devkit from '@nx/devkit';
const { readCachedProjectGraph } = devkit; const { readCachedProjectGraph } = devkit;
function invariant(condition, message) { function invariant(condition, message) {
if (!condition) { if (!condition) {
console.error(chalk.bold.red(message)); console.error(message);
process.exit(1); process.exit(1);
} }
} }
@ -58,9 +57,7 @@ try {
json.version = version; json.version = version;
writeFileSync(\`package.json\`, JSON.stringify(json, null, 2)); writeFileSync(\`package.json\`, JSON.stringify(json, null, 2));
} catch (e) { } catch (e) {
console.error( console.error(\`Error reading package.json file from library build output.\`);
chalk.bold.red(\`Error reading package.json file from library build output.\`)
);
} }
// Execute "npm publish" to publish // Execute "npm publish" to publish

View File

@ -11,13 +11,13 @@ import {
GeneratorCallback, GeneratorCallback,
runTasksInSerial, runTasksInSerial,
joinPathFragments, joinPathFragments,
getProjects,
} from '@nx/devkit'; } from '@nx/devkit';
import { libraryGenerator as jsLibraryGenerator } from '@nx/js'; import { libraryGenerator as jsLibraryGenerator } from '@nx/js';
import { nxVersion } from 'nx/src/utils/versions'; import { nxVersion } from 'nx/src/utils/versions';
import generatorGenerator from '../generator/generator'; import generatorGenerator from '../generator/generator';
import { CreatePackageSchema } from './schema'; import { CreatePackageSchema } from './schema';
import { NormalizedSchema, normalizeSchema } from './utils/normalize-schema'; import { NormalizedSchema, normalizeSchema } from './utils/normalize-schema';
import e2eProjectGenerator from '../e2e-project/e2e';
import { hasGenerator } from '../../utils/has-generator'; import { hasGenerator } from '../../utils/has-generator';
export async function createPackageGenerator( export async function createPackageGenerator(
@ -39,6 +39,9 @@ export async function createPackageGenerator(
tasks.push(installTask); tasks.push(installTask);
await createCliPackage(host, options, pluginPackageName); await createCliPackage(host, options, pluginPackageName);
if (options.e2eProject) {
addE2eProject(host, options);
}
if (!options.skipFormat) { if (!options.skipFormat) {
await formatFiles(host); await formatFiles(host);
@ -149,57 +152,22 @@ async function createCliPackage(
* @param options * @param options
* @returns * @returns
*/ */
async function addE2eProject(host: Tree, options: NormalizedSchema) { 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( const e2eProjectConfiguration = readProjectConfiguration(
host, host,
`${options.projectName}-e2e` options.e2eProject
); );
e2eProjectConfiguration.targets.e2e.dependsOn = ['^build'];
updateProjectConfiguration(
host,
e2eProjectConfiguration.name,
e2eProjectConfiguration
);
// delete the default e2e test file
host.delete(e2eProjectConfiguration.sourceRoot);
generateFiles( generateFiles(
host, host,
joinPathFragments(__dirname, './files/e2e'), joinPathFragments(__dirname, './files/e2e'),
e2eProjectConfiguration.sourceRoot, e2eProjectConfiguration.sourceRoot,
{ {
...options, pluginName: options.project,
pluginOutputPath, cliName: options.name,
cliOutputPath,
tmpl: '', tmpl: '',
} }
); );
return e2eTask;
} }
export default createPackageGenerator; export default createPackageGenerator;

View File

@ -0,0 +1,31 @@
import {
checkFilesExist,
removeTmpProject,
tmpFolder,
uniq,
} from '@nx/plugin/testing';
import { execSync } from 'child_process';
import { join } from 'path';
describe('<%= cliName %> e2e', () => {
let project: string;
beforeAll(async () => {
// create a workspace with cli
project = uniq('<%= cliName %>');
execSync(`npx <%= cliName %>@1.0.0 ${project}`, {
cwd: tmpFolder(),
env: process.env,
});
}, 240_000);
afterAll(() => {
// Remove the generated project from the file system
removeTmpProject(project);
});
it('should create project using <%= cliName %>', () => {
expect(() =>
checkFilesExist(join(tmpFolder(), project, 'package.json'))
).not.toThrow();
});
});

View File

@ -1,29 +0,0 @@
import {
runCreatePackageCli,
removeTmpProject,
} from '@nx/plugin/testing';
describe('<%= name %> e2e', () => {
const project = '<%= 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

@ -13,5 +13,5 @@ export interface CreatePackageSchema {
compiler: 'swc' | 'tsc'; compiler: 'swc' | 'tsc';
// options to create e2e project, passed to e2e project generator // options to create e2e project, passed to e2e project generator
e2eTestRunner?: 'jest' | 'none'; e2eProject?: string;
} }

View File

@ -59,11 +59,14 @@
"default": "tsc", "default": "tsc",
"description": "The compiler used by the build and test targets." "description": "The compiler used by the build and test targets."
}, },
"e2eTestRunner": { "e2eProject": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "description": "The name of the e2e project.",
"description": "Test runner to use for end to end (E2E) tests.", "alias": "p",
"default": "jest" "$default": {
"$source": "projectName"
},
"x-prompt": "What is the name of the e2e project?"
} }
}, },
"required": ["name", "project"] "required": ["name", "project"]

View File

@ -9,16 +9,19 @@ import {
getWorkspaceLayout, getWorkspaceLayout,
joinPathFragments, joinPathFragments,
names, names,
offsetFromRoot,
readProjectConfiguration, readProjectConfiguration,
runTasksInSerial, runTasksInSerial,
updateProjectConfiguration, updateProjectConfiguration,
} from '@nx/devkit'; } from '@nx/devkit';
import { jestProjectGenerator } from '@nx/jest'; import { addPropertyToJestConfig, jestProjectGenerator } from '@nx/jest';
import { getRelativePathToRootTsConfig } from '@nx/js'; import { getRelativePathToRootTsConfig } from '@nx/js';
import * as path from 'path'; import { setupVerdaccio } from '@nx/js/src/generators/setup-verdaccio/generator';
import { addLocalRegistryScripts } from '@nx/js/src/utils/add-local-registry-scripts';
import { join } from 'path';
import { Linter, lintProjectGenerator } from '@nx/linter';
import type { Schema } from './schema'; import type { Schema } from './schema';
import { Linter, lintProjectGenerator } from '@nx/linter';
interface NormalizedSchema extends Schema { interface NormalizedSchema extends Schema {
projectRoot: string; projectRoot: string;
@ -63,7 +66,7 @@ function validatePlugin(host: Tree, pluginName: string) {
} }
function addFiles(host: Tree, options: NormalizedSchema) { function addFiles(host: Tree, options: NormalizedSchema) {
generateFiles(host, path.join(__dirname, './files'), options.projectRoot, { generateFiles(host, join(__dirname, './files'), options.projectRoot, {
...options, ...options,
tmpl: '', tmpl: '',
rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot), rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot),
@ -87,6 +90,22 @@ async function addJest(host: Tree, options: NormalizedSchema) {
skipFormat: true, skipFormat: true,
}); });
const { startLocalRegistryPath, stopLocalRegistryPath } =
addLocalRegistryScripts(host);
addPropertyToJestConfig(
host,
join(options.projectRoot, 'jest.config.ts'),
'globalSetup',
join(offsetFromRoot(options.projectRoot), startLocalRegistryPath)
);
addPropertyToJestConfig(
host,
join(options.projectRoot, 'jest.config.ts'),
'globalTeardown',
join(offsetFromRoot(options.projectRoot), stopLocalRegistryPath)
);
const project = readProjectConfiguration(host, options.projectName); const project = readProjectConfiguration(host, options.projectName);
const testTarget = project.targets.test; const testTarget = project.targets.test;
@ -133,6 +152,11 @@ export async function e2eProjectGenerator(host: Tree, schema: Schema) {
validatePlugin(host, schema.pluginName); validatePlugin(host, schema.pluginName);
const options = normalizeOptions(host, schema); const options = normalizeOptions(host, schema);
addFiles(host, options); addFiles(host, options);
tasks.push(
await setupVerdaccio(host, {
skipFormat: true,
})
);
tasks.push(await addJest(host, options)); tasks.push(await addJest(host, options));
if (options.linter !== Linter.None) { if (options.linter !== Linter.None) {

View File

@ -1,7 +1,6 @@
import { import {
checkFilesExist, checkFilesExist,
ensureNxProject, ensureNxProject,
readJson,
runNxCommandAsync, runNxCommandAsync,
runNxCommand, runNxCommand,
} from '@nx/plugin/testing'; } from '@nx/plugin/testing';

View File

@ -1,72 +0,0 @@
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,6 +1,5 @@
export * from './async-commands'; export * from './async-commands';
export * from './commands'; export * from './commands';
export * from './create-package-cli';
export * from './paths'; export * from './paths';
export * from './nx-project'; export * from './nx-project';
export * from './utils'; export * from './utils';