diff --git a/docs/generated/packages/plugin/generators/create-package.json b/docs/generated/packages/plugin/generators/create-package.json index d226f73bee..31d0171dbd 100644 --- a/docs/generated/packages/plugin/generators/create-package.json +++ b/docs/generated/packages/plugin/generators/create-package.json @@ -57,11 +57,12 @@ "default": "tsc", "description": "The compiler used by the build and test targets." }, - "e2eTestRunner": { + "e2eProject": { "type": "string", - "enum": ["jest", "none"], - "description": "Test runner to use for end to end (E2E) tests.", - "default": "jest" + "description": "The name of the e2e project.", + "alias": "p", + "$default": { "$source": "projectName" }, + "x-prompt": "What is the name of the e2e project?" } }, "required": ["name", "project"], diff --git a/e2e/utils/global-setup.ts b/e2e/utils/global-setup.ts index 10039a96f3..979043a19f 100644 --- a/e2e/utils/global-setup.ts +++ b/e2e/utils/global-setup.ts @@ -22,7 +22,7 @@ export default async function (globalConfig: Config.ConfigGlobals) { { stdio: 'pipe' } ); - childProcess?.stdout?.on('data', (data) => { + const listener = (data) => { if (data.toString().includes('http://localhost:')) { const port = parseInt( data.toString().match(/localhost:(?\d+)/)?.groups?.port @@ -35,11 +35,12 @@ export default async function (globalConfig: Config.ConfigGlobals) { console.log('Set npm and yarn config registry to ' + registry); resolve(childProcess); + childProcess.stdout?.off('data', listener); } - }); + }; + childProcess?.stdout?.on('data', listener); childProcess?.stderr?.on('data', (data) => { process.stderr.write(data); - reject(data); }); childProcess.on('error', (err) => { console.log('local registry error', err); diff --git a/e2e/workspace-create/src/create-nx-plugin.test.ts b/e2e/workspace-create/src/create-nx-plugin.test.ts index ac46967219..5348a90e4a 100644 --- a/e2e/workspace-create/src/create-nx-plugin.test.ts +++ b/e2e/workspace-create/src/create-nx-plugin.test.ts @@ -6,7 +6,6 @@ import { uniq, runCreatePlugin, cleanupProject, - tmpProjPath, } from '@nx/e2e/utils'; describe('create-nx-plugin', () => { @@ -64,7 +63,8 @@ describe('create-nx-plugin', () => { runCLI(`build ${pluginName}`); checkFilesExist( `dist/${pluginName}/package.json`, - `dist/${pluginName}/generators.json` + `dist/${pluginName}/generators.json`, + `e2e/tests/${pluginName}.spec.ts` ); runCLI(`build create-${pluginName}-package`); diff --git a/packages/js/plugins/jest/local-registry.ts b/packages/js/plugins/jest/local-registry.ts new file mode 100644 index 0000000000..f1bcd1f6aa --- /dev/null +++ b/packages/js/plugins/jest/local-registry.ts @@ -0,0 +1 @@ +export * from '../../src/plugins/jest/start-local-registry'; diff --git a/packages/js/src/executors/verdaccio/verdaccio.impl.ts b/packages/js/src/executors/verdaccio/verdaccio.impl.ts index 790bdadafa..30fee7d469 100644 --- a/packages/js/src/executors/verdaccio/verdaccio.impl.ts +++ b/packages/js/src/executors/verdaccio/verdaccio.impl.ts @@ -1,8 +1,8 @@ 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 * as detectPort from 'detect-port'; -import { join } from 'path'; +import { join, resolve } from 'path'; import { VerdaccioExecutorSchema } from './schema'; @@ -25,8 +25,12 @@ export async function verdaccioExecutor( ); } - if (options.clear && options.storage && existsSync(options.storage)) { - removeSync(options.storage); + if (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 = @@ -84,23 +88,10 @@ function startVerdaccio( ? { 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) => { reject(err); }); diff --git a/packages/js/src/generators/setup-verdaccio/files/config.yml b/packages/js/src/generators/setup-verdaccio/files/config.yml index 1805e5ac80..6efc9446a0 100644 --- a/packages/js/src/generators/setup-verdaccio/files/config.yml +++ b/packages/js/src/generators/setup-verdaccio/files/config.yml @@ -8,7 +8,7 @@ auth: # a list of other known repositories we can talk to uplinks: npmjs: - url: https://registry.npmjs.org/ + url: <%= npmUplinkRegistry %> maxage: 60m packages: diff --git a/packages/js/src/generators/setup-verdaccio/generator.ts b/packages/js/src/generators/setup-verdaccio/generator.ts index dd27e05ffd..8b452fa487 100644 --- a/packages/js/src/generators/setup-verdaccio/generator.ts +++ b/packages/js/src/generators/setup-verdaccio/generator.ts @@ -13,13 +13,18 @@ import { import * as path from 'path'; import { SetupVerdaccioGeneratorSchema } from './schema'; import { verdaccioVersion } from '../../utils/versions'; +import { execSync } from 'child_process'; export async function setupVerdaccio( tree: Tree, options: SetupVerdaccioGeneratorSchema ) { 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 = { diff --git a/packages/js/src/plugins/jest/start-local-registry.ts b/packages/js/src/plugins/jest/start-local-registry.ts new file mode 100644 index 0000000000..d11352950d --- /dev/null +++ b/packages/js/src/plugins/jest/start-local-registry.ts @@ -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:(?\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; diff --git a/packages/js/src/utils/add-local-registry-scripts.ts b/packages/js/src/utils/add-local-registry-scripts.ts new file mode 100644 index 0000000000..0ef34f266b --- /dev/null +++ b/packages/js/src/utils/add-local-registry-scripts.ts @@ -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 }; +} diff --git a/packages/js/src/utils/minimal-publish-script.ts b/packages/js/src/utils/minimal-publish-script.ts index de45759e12..9715810ca4 100644 --- a/packages/js/src/utils/minimal-publish-script.ts +++ b/packages/js/src/utils/minimal-publish-script.ts @@ -12,14 +12,13 @@ const publishScriptContent = ` import { execSync } from 'child_process'; import { readFileSync, writeFileSync } from 'fs'; -import chalk from 'chalk'; import devkit from '@nx/devkit'; const { readCachedProjectGraph } = devkit; function invariant(condition, message) { if (!condition) { - console.error(chalk.bold.red(message)); + console.error(message); process.exit(1); } } @@ -58,9 +57,7 @@ try { json.version = version; writeFileSync(\`package.json\`, JSON.stringify(json, null, 2)); } catch (e) { - console.error( - chalk.bold.red(\`Error reading package.json file from library build output.\`) - ); + console.error(\`Error reading package.json file from library build output.\`); } // Execute "npm publish" to publish diff --git a/packages/plugin/src/generators/create-package/create-package.ts b/packages/plugin/src/generators/create-package/create-package.ts index b143cb54d2..1a3ece6003 100644 --- a/packages/plugin/src/generators/create-package/create-package.ts +++ b/packages/plugin/src/generators/create-package/create-package.ts @@ -11,13 +11,13 @@ import { GeneratorCallback, runTasksInSerial, joinPathFragments, + getProjects, } 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( @@ -39,6 +39,9 @@ export async function createPackageGenerator( tasks.push(installTask); await createCliPackage(host, options, pluginPackageName); + if (options.e2eProject) { + addE2eProject(host, options); + } if (!options.skipFormat) { await formatFiles(host); @@ -149,57 +152,22 @@ async function createCliPackage( * @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, - }); - +function addE2eProject(host: Tree, options: NormalizedSchema) { const e2eProjectConfiguration = readProjectConfiguration( 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( host, joinPathFragments(__dirname, './files/e2e'), e2eProjectConfiguration.sourceRoot, { - ...options, - pluginOutputPath, - cliOutputPath, + pluginName: options.project, + cliName: options.name, tmpl: '', } ); - - return e2eTask; } export default createPackageGenerator; diff --git a/packages/plugin/src/generators/create-package/files/e2e/__cliName__.spec.ts__tmpl__ b/packages/plugin/src/generators/create-package/files/e2e/__cliName__.spec.ts__tmpl__ new file mode 100644 index 0000000000..b9f7d17227 --- /dev/null +++ b/packages/plugin/src/generators/create-package/files/e2e/__cliName__.spec.ts__tmpl__ @@ -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(); + }); +}); diff --git a/packages/plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ b/packages/plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ deleted file mode 100644 index 1ebe1bdad5..0000000000 --- a/packages/plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ +++ /dev/null @@ -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'); - }); -}); diff --git a/packages/plugin/src/generators/create-package/schema.d.ts b/packages/plugin/src/generators/create-package/schema.d.ts index 6acf4c1f77..7973977a79 100644 --- a/packages/plugin/src/generators/create-package/schema.d.ts +++ b/packages/plugin/src/generators/create-package/schema.d.ts @@ -13,5 +13,5 @@ export interface CreatePackageSchema { compiler: 'swc' | 'tsc'; // options to create e2e project, passed to e2e project generator - e2eTestRunner?: 'jest' | 'none'; + e2eProject?: string; } diff --git a/packages/plugin/src/generators/create-package/schema.json b/packages/plugin/src/generators/create-package/schema.json index 8125d63f5a..73a7bc0e24 100644 --- a/packages/plugin/src/generators/create-package/schema.json +++ b/packages/plugin/src/generators/create-package/schema.json @@ -59,11 +59,14 @@ "default": "tsc", "description": "The compiler used by the build and test targets." }, - "e2eTestRunner": { + "e2eProject": { "type": "string", - "enum": ["jest", "none"], - "description": "Test runner to use for end to end (E2E) tests.", - "default": "jest" + "description": "The name of the e2e project.", + "alias": "p", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What is the name of the e2e project?" } }, "required": ["name", "project"] diff --git a/packages/plugin/src/generators/e2e-project/e2e.ts b/packages/plugin/src/generators/e2e-project/e2e.ts index 3573559c93..ce799a7af3 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.ts @@ -9,16 +9,19 @@ import { getWorkspaceLayout, joinPathFragments, names, + offsetFromRoot, readProjectConfiguration, runTasksInSerial, updateProjectConfiguration, } from '@nx/devkit'; -import { jestProjectGenerator } from '@nx/jest'; +import { addPropertyToJestConfig, jestProjectGenerator } from '@nx/jest'; 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 { Linter, lintProjectGenerator } from '@nx/linter'; interface NormalizedSchema extends Schema { projectRoot: string; @@ -63,7 +66,7 @@ function validatePlugin(host: Tree, pluginName: string) { } function addFiles(host: Tree, options: NormalizedSchema) { - generateFiles(host, path.join(__dirname, './files'), options.projectRoot, { + generateFiles(host, join(__dirname, './files'), options.projectRoot, { ...options, tmpl: '', rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot), @@ -87,6 +90,22 @@ async function addJest(host: Tree, options: NormalizedSchema) { 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 testTarget = project.targets.test; @@ -133,6 +152,11 @@ export async function e2eProjectGenerator(host: Tree, schema: Schema) { validatePlugin(host, schema.pluginName); const options = normalizeOptions(host, schema); addFiles(host, options); + tasks.push( + await setupVerdaccio(host, { + skipFormat: true, + }) + ); tasks.push(await addJest(host, options)); if (options.linter !== Linter.None) { diff --git a/packages/plugin/src/generators/e2e-project/files/tests/__pluginName__.spec.ts__tmpl__ b/packages/plugin/src/generators/e2e-project/files/tests/__pluginName__.spec.ts__tmpl__ index f966c26e75..ddeb7327dd 100644 --- a/packages/plugin/src/generators/e2e-project/files/tests/__pluginName__.spec.ts__tmpl__ +++ b/packages/plugin/src/generators/e2e-project/files/tests/__pluginName__.spec.ts__tmpl__ @@ -1,7 +1,6 @@ import { checkFilesExist, ensureNxProject, - readJson, runNxCommandAsync, runNxCommand, } from '@nx/plugin/testing'; diff --git a/packages/plugin/src/utils/testing-utils/create-package-cli.ts b/packages/plugin/src/utils/testing-utils/create-package-cli.ts deleted file mode 100644 index b983fe590f..0000000000 --- a/packages/plugin/src/utils/testing-utils/create-package-cli.ts +++ /dev/null @@ -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 { - 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}`; -} diff --git a/packages/plugin/src/utils/testing-utils/index.ts b/packages/plugin/src/utils/testing-utils/index.ts index 2541b07012..81414ad685 100644 --- a/packages/plugin/src/utils/testing-utils/index.ts +++ b/packages/plugin/src/utils/testing-utils/index.ts @@ -1,6 +1,5 @@ export * from './async-commands'; export * from './commands'; -export * from './create-package-cli'; export * from './paths'; export * from './nx-project'; export * from './utils';