diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 356d3d198dd2a..b0bbfa06b395f 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -5027,6 +5027,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "verdaccio", + "path": "/packages/js/executors/verdaccio", + "name": "verdaccio", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, @@ -5060,6 +5068,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "setup-verdaccio", + "path": "/packages/js/generators/setup-verdaccio", + "name": "setup-verdaccio", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/packages.json b/docs/generated/manifests/packages.json index 53704f8840e3c..35123dcb3377f 100644 --- a/docs/generated/manifests/packages.json +++ b/docs/generated/manifests/packages.json @@ -1002,6 +1002,15 @@ "originalFilePath": "/packages/js/src/executors/node/schema.json", "path": "/packages/js/executors/node", "type": "executor" + }, + "/packages/js/executors/verdaccio": { + "description": "Start local registry with verdaccio", + "file": "generated/packages/js/executors/verdaccio.json", + "hidden": false, + "name": "verdaccio", + "originalFilePath": "/packages/js/src/executors/verdaccio/schema.json", + "path": "/packages/js/executors/verdaccio", + "type": "executor" } }, "generators": { @@ -1031,6 +1040,15 @@ "originalFilePath": "/packages/js/src/generators/convert-to-swc/schema.json", "path": "/packages/js/generators/convert-to-swc", "type": "generator" + }, + "/packages/js/generators/setup-verdaccio": { + "description": "Setup Verdaccio for local package management.", + "file": "generated/packages/js/generators/setup-verdaccio.json", + "hidden": false, + "name": "setup-verdaccio", + "originalFilePath": "/packages/js/src/generators/setup-verdaccio/schema.json", + "path": "/packages/js/generators/setup-verdaccio", + "type": "generator" } }, "path": "/packages/js" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 8770b0085ff90..46d467ff50619 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -986,6 +986,15 @@ "originalFilePath": "/packages/js/src/executors/node/schema.json", "path": "js/executors/node", "type": "executor" + }, + { + "description": "Start local registry with verdaccio", + "file": "generated/packages/js/executors/verdaccio.json", + "hidden": false, + "name": "verdaccio", + "originalFilePath": "/packages/js/src/executors/verdaccio/schema.json", + "path": "js/executors/verdaccio", + "type": "executor" } ], "generators": [ @@ -1015,6 +1024,15 @@ "originalFilePath": "/packages/js/src/generators/convert-to-swc/schema.json", "path": "js/generators/convert-to-swc", "type": "generator" + }, + { + "description": "Setup Verdaccio for local package management.", + "file": "generated/packages/js/generators/setup-verdaccio.json", + "hidden": false, + "name": "setup-verdaccio", + "originalFilePath": "/packages/js/src/generators/setup-verdaccio/schema.json", + "path": "js/generators/setup-verdaccio", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/js/executors/verdaccio.json b/docs/generated/packages/js/executors/verdaccio.json new file mode 100644 index 0000000000000..931ce53e72e27 --- /dev/null +++ b/docs/generated/packages/js/executors/verdaccio.json @@ -0,0 +1,45 @@ +{ + "name": "verdaccio", + "implementation": "/packages/js/src/executors/verdaccio/verdaccio.impl.ts", + "schema": { + "$schema": "http://json-schema.org/schema", + "version": 2, + "title": "Verdaccio Local Registry", + "description": "Start a local registry with Verdaccio.", + "cli": "nx", + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "Location option for npm config", + "default": "user", + "enum": ["global", "user", "project"] + }, + "storage": { + "type": "string", + "description": "Path to the custom storage directory for Verdaccio" + }, + "port": { + "type": "number", + "description": "Port of local registry that Verdaccio should listen to", + "default": 4873 + }, + "config": { + "type": "string", + "description": "Path to the custom Verdaccio config file" + }, + "clear": { + "type": "boolean", + "description": "Clear local registry storage before starting Verdaccio", + "default": true + } + }, + "required": ["port"], + "presets": [] + }, + "description": "Start local registry with verdaccio", + "aliases": [], + "hidden": false, + "path": "/packages/js/src/executors/verdaccio/schema.json", + "type": "executor" +} diff --git a/docs/generated/packages/js/generators/setup-verdaccio.json b/docs/generated/packages/js/generators/setup-verdaccio.json new file mode 100644 index 0000000000000..be48e303fa5ce --- /dev/null +++ b/docs/generated/packages/js/generators/setup-verdaccio.json @@ -0,0 +1,28 @@ +{ + "name": "setup-verdaccio", + "factory": "./src/generators/setup-verdaccio/generator#setupVerdaccio", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "SetupVerdaccio", + "title": "Setup Verdaccio", + "description": "Setup Verdaccio local-registry.", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + }, + "required": [], + "presets": [] + }, + "alias": ["verdaccio"], + "description": "Setup Verdaccio for local package management.", + "implementation": "/packages/js/src/generators/setup-verdaccio/generator#setupVerdaccio.ts", + "aliases": [], + "hidden": false, + "path": "/packages/js/src/generators/setup-verdaccio/schema.json", + "type": "generator" +} diff --git a/packages/js/executors.json b/packages/js/executors.json index bfedf70d9d752..4f749eff6ebb2 100644 --- a/packages/js/executors.json +++ b/packages/js/executors.json @@ -15,6 +15,11 @@ "implementation": "./src/executors/node/node.impl", "schema": "./src/executors/node/schema.json", "description": "Execute a Node application." + }, + "verdaccio": { + "implementation": "./src/executors/verdaccio/verdaccio.impl", + "schema": "./src/executors/verdaccio/schema.json", + "description": "Start local registry with verdaccio" } }, "builders": { @@ -32,6 +37,11 @@ "implementation": "./src/executors/node/compat", "schema": "./src/executors/node/schema.json", "description": "Execute a Node application." + }, + "verdaccio": { + "implementation": "./src/executors/verdaccio/compat", + "schema": "./src/executors/verdaccio/schema.json", + "description": "Start local registry with verdaccio" } } } diff --git a/packages/js/generators.json b/packages/js/generators.json index 1913a999db978..3b48d62a48af4 100644 --- a/packages/js/generators.json +++ b/packages/js/generators.json @@ -23,6 +23,12 @@ "aliases": ["swc"], "x-type": "library", "description": "Convert a TypeScript library to compile with SWC." + }, + "setup-verdaccio": { + "factory": "./src/generators/setup-verdaccio/generator#setupVerdaccioSchematic", + "schema": "./src/generators/setup-verdaccio/schema.json", + "alias": ["verdaccio"], + "description": "Setup Verdaccio for local package management." } }, "generators": { @@ -47,6 +53,12 @@ "aliases": ["swc"], "x-type": "library", "description": "Convert a TypeScript library to compile with SWC." + }, + "setup-verdaccio": { + "factory": "./src/generators/setup-verdaccio/generator#setupVerdaccio", + "schema": "./src/generators/setup-verdaccio/schema.json", + "alias": ["verdaccio"], + "description": "Setup Verdaccio for local package management." } } } diff --git a/packages/js/package.json b/packages/js/package.json index 64881b56cc4c7..79cdfb2880802 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -54,6 +54,14 @@ "@nx/devkit": "file:../devkit", "@nx/workspace": "file:../workspace" }, + "peerDependencies": { + "verdaccio": "^5.0.4" + }, + "peerDependenciesMeta": { + "verdaccio": { + "optional": true + } + }, "publishConfig": { "access": "public" } diff --git a/packages/js/src/executors/verdaccio/compat.ts b/packages/js/src/executors/verdaccio/compat.ts new file mode 100644 index 0000000000000..59c9b9ce7d167 --- /dev/null +++ b/packages/js/src/executors/verdaccio/compat.ts @@ -0,0 +1,5 @@ +import { convertNxExecutor } from '@nx/devkit'; + +import { verdaccioExecutor } from './verdaccio.impl'; + +export default convertNxExecutor(verdaccioExecutor); diff --git a/packages/js/src/executors/verdaccio/schema.d.ts b/packages/js/src/executors/verdaccio/schema.d.ts new file mode 100644 index 0000000000000..0744a5d8d1af4 --- /dev/null +++ b/packages/js/src/executors/verdaccio/schema.d.ts @@ -0,0 +1,7 @@ +export interface VerdaccioExecutorSchema { + location?: string; + storage?: string; + port: number; + config?: string; + clear?: boolean; +} diff --git a/packages/js/src/executors/verdaccio/schema.json b/packages/js/src/executors/verdaccio/schema.json new file mode 100644 index 0000000000000..a6b63a650db23 --- /dev/null +++ b/packages/js/src/executors/verdaccio/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/schema", + "version": 2, + "title": "Verdaccio Local Registry", + "description": "Start a local registry with Verdaccio.", + "cli": "nx", + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "Location option for npm config", + "default": "user", + "enum": ["global", "user", "project"] + }, + "storage": { + "type": "string", + "description": "Path to the custom storage directory for Verdaccio" + }, + "port": { + "type": "number", + "description": "Port of local registry that Verdaccio should listen to", + "default": 4873 + }, + "config": { + "type": "string", + "description": "Path to the custom Verdaccio config file" + }, + "clear": { + "type": "boolean", + "description": "Clear local registry storage before starting Verdaccio", + "default": true + } + }, + "required": ["port"] +} diff --git a/packages/js/src/executors/verdaccio/verdaccio.impl.ts b/packages/js/src/executors/verdaccio/verdaccio.impl.ts new file mode 100644 index 0000000000000..16e76b7227faa --- /dev/null +++ b/packages/js/src/executors/verdaccio/verdaccio.impl.ts @@ -0,0 +1,192 @@ +import { ExecutorContext, logger } from '@nx/devkit'; +import { removeSync, existsSync } from 'fs-extra'; +import { ChildProcess, execSync, fork } from 'child_process'; +import { VerdaccioExecutorSchema } from './schema'; + +let childProcess: ChildProcess; + +/** + * - set npm and yarn to use local registry + * - start verdaccio + * - stop local registry when done + */ +export async function verdaccioExecutor( + options: VerdaccioExecutorSchema, + context: ExecutorContext +) { + try { + require.resolve('verdaccio'); + } catch (e) { + throw new Error( + 'Verdaccio is not installed. Please run `npm install verdaccio` or `yarn add verdaccio`' + ); + } + + if (options.clear && options.storage && existsSync(options.storage)) { + removeSync(options.storage); + } + const cleanupFunctions = [setupNpm(options), setupYarn(options)]; + + const processExitListener = (signal?: number | NodeJS.Signals) => { + if (childProcess) { + childProcess.kill(signal); + } + for (const fn of cleanupFunctions) { + fn(); + } + }; + process.on('exit', processExitListener); + process.on('SIGTERM', processExitListener); + process.on('SIGINT', processExitListener); + process.on('SIGHUP', processExitListener); + + try { + await startVerdaccio(options); + } catch (e) { + logger.error('Failed to start verdaccio: ' + e.toString()); + return { + success: false, + }; + } + return { + success: true, + }; +} + +/** + * Fork the verdaccio process: https://verdaccio.org/docs/verdaccio-programmatically/#using-fork-from-child_process-module + */ +function startVerdaccio(options: VerdaccioExecutorSchema) { + return new Promise((resolve, reject) => { + childProcess = fork( + require.resolve('verdaccio/bin/verdaccio'), + createVerdaccioOptions(options), + { + env: { + ...process.env, + VERDACCIO_HANDLE_KILL_SIGNALS: 'true', + }, + stdio: ['inherit', 'pipe', 'pipe', 'ipc'], + } + ); + + 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); + }); + childProcess.on('disconnect', (err) => { + reject(err); + }); + childProcess.on('exit', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(code); + } + }); + }); +} + +function createVerdaccioOptions(options: VerdaccioExecutorSchema) { + const verdaccioArgs: string[] = []; + if (options.port) { + verdaccioArgs.push('--listen', options.port.toString()); + } + if (options.config) { + verdaccioArgs.push('--config', options.config); + } + return verdaccioArgs; +} + +function setupNpm(options: VerdaccioExecutorSchema) { + try { + execSync('npm --version'); + } catch (e) { + return () => {}; + } + + let npmRegistryPath; + try { + npmRegistryPath = execSync( + `npm config get registry --location ${options.location}` + ) + ?.toString() + ?.trim() + ?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes + execSync( + `npm config set registry http://localhost:${options.port}/ --location ${options.location}` + ); + execSync( + `npm config set //localhost:${options.port}/:_authToken="secretVerdaccioToken"` + ); + logger.info(`Set npm registry to http://localhost:${options.port}/`); + } catch (e) { + throw new Error( + `Failed to set npm registry to http://localhost:${options.port}/: ${e.message}` + ); + } + + return () => { + try { + if (npmRegistryPath) { + execSync( + `npm config set registry ${npmRegistryPath} --location ${options.location}` + ); + logger.info(`Reset npm registry to ${npmRegistryPath}`); + } else { + execSync(`npm config delete registry --location ${options.location}`); + } + } catch (e) { + throw new Error(`Failed to reset npm registry: ${e.message}`); + } + }; +} + +function setupYarn(options: VerdaccioExecutorSchema) { + try { + execSync('yarn --version'); + } catch (e) { + return () => {}; + } + + let yarnRegistryPath; + try { + yarnRegistryPath = execSync(`yarn config get registry`) + ?.toString() + ?.trim() + ?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes + execSync(`yarn config set registry http://localhost:${options.port}/`); + logger.info(`Set yarn registry to http://localhost:${options.port}/`); + } catch (e) { + throw new Error( + `Failed to set yarn registry to http://localhost:${options.port}/: ${e.message}` + ); + } + + return () => { + try { + if (yarnRegistryPath) { + execSync(`yarn config set registry ${yarnRegistryPath}`); + logger.info(`Reset yarn registry to ${yarnRegistryPath}`); + } else { + execSync(`yarn config delete registry`); + } + } catch (e) { + throw new Error(`Failed to reset yarn registry: ${e.message}`); + } + }; +} + +export default verdaccioExecutor; diff --git a/packages/js/src/generators/init/init.ts b/packages/js/src/generators/init/init.ts index 86936f80bb1a2..53093f09b2142 100644 --- a/packages/js/src/generators/init/init.ts +++ b/packages/js/src/generators/init/init.ts @@ -19,8 +19,6 @@ import { } from '../../utils/versions'; import { InitSchema } from './schema'; -let formatTaskAdded = false; - export async function initGenerator( tree: Tree, schema: InitSchema diff --git a/packages/js/src/generators/library/library.ts b/packages/js/src/generators/library/library.ts index f846fa7ebbdd4..a3b386098c64a 100644 --- a/packages/js/src/generators/library/library.ts +++ b/packages/js/src/generators/library/library.ts @@ -36,6 +36,7 @@ import { } from '../../utils/versions'; import jsInitGenerator from '../init/init'; import { PackageJson } from 'nx/src/utils/package-json'; +import setupVerdaccio from '../setup-verdaccio/generator'; export async function libraryGenerator( tree: Tree, @@ -72,6 +73,10 @@ export async function projectGenerator( tasks.push(addProjectDependencies(tree, options)); + if (options.publishable) { + tasks.push(await setupVerdaccio(tree, { ...options, skipFormat: true })); + } + if (options.bundler === 'vite') { const { viteConfigurationGenerator } = ensurePackage('@nx/vite', nxVersion); const viteTask = await viteConfigurationGenerator(tree, { diff --git a/packages/js/src/generators/setup-verdaccio/files/config.yml b/packages/js/src/generators/setup-verdaccio/files/config.yml new file mode 100644 index 0000000000000..1805e5ac8097d --- /dev/null +++ b/packages/js/src/generators/setup-verdaccio/files/config.yml @@ -0,0 +1,29 @@ +# path to a directory with all packages +storage: ../tmp/local-registry/storage + +auth: + htpasswd: + file: ./htpasswd + +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + maxage: 60m + +packages: + '**': + # give all users (including non-authenticated users) full access + # because it is a local registry + access: $all + publish: $all + unpublish: $all + + # if package is not available locally, proxy requests to npm registry + proxy: npmjs + +# log settings +logs: + type: stdout + format: pretty + level: http diff --git a/packages/js/src/generators/setup-verdaccio/files/htpasswd b/packages/js/src/generators/setup-verdaccio/files/htpasswd new file mode 100644 index 0000000000000..8391cd4b0a901 --- /dev/null +++ b/packages/js/src/generators/setup-verdaccio/files/htpasswd @@ -0,0 +1 @@ +test:$6FrCaT/v0dwE:autocreated 2020-03-25T19:10:50.254Z diff --git a/packages/js/src/generators/setup-verdaccio/generator.spec.ts b/packages/js/src/generators/setup-verdaccio/generator.spec.ts new file mode 100644 index 0000000000000..40d6f276624a5 --- /dev/null +++ b/packages/js/src/generators/setup-verdaccio/generator.spec.ts @@ -0,0 +1,97 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { Tree, readJson, readProjectConfiguration } from '@nx/devkit'; + +import generator from './generator'; +import { SetupVerdaccioGeneratorSchema } from './schema'; +import { PackageJson } from 'nx/src/utils/package-json'; + +describe('setup-verdaccio generator', () => { + let tree: Tree; + const options: SetupVerdaccioGeneratorSchema = { skipFormat: false }; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should create project.json if it does not exist', async () => { + await generator(tree, options); + const config = readJson(tree, 'project.json'); + expect(config).toEqual({ + name: 'test-name', + $schema: 'node_modules/nx/schemas/project-schema.json', + targets: { + 'local-registry': { + executor: '@nx/js:verdaccio', + options: { + port: 4873, + config: '.verdaccio/config.yml', + storage: 'tmp/local-registry/storage', + }, + }, + }, + }); + }); + + it('should add local-registry target to project.json', async () => { + tree.write('project.json', JSON.stringify({})); + await generator(tree, options); + const config = readJson(tree, 'project.json'); + expect(config).toEqual({ + targets: { + 'local-registry': { + executor: '@nx/js:verdaccio', + options: { + port: 4873, + config: '.verdaccio/config.yml', + storage: 'tmp/local-registry/storage', + }, + }, + }, + }); + }); + + it('should not override existing local-registry target to project.json if target already exists', async () => { + tree.write( + 'project.json', + JSON.stringify({ + targets: { + 'local-registry': {}, + }, + }) + ); + await generator(tree, options); + const config = readJson(tree, 'project.json'); + expect(config).toEqual({ + targets: { + 'local-registry': {}, + }, + }); + }); + + it('should be able to run setup verdaccio multiple times', async () => { + await generator(tree, options); + tree.write( + 'project.json', + JSON.stringify({ + targets: { + 'local-registry': {}, + }, + }) + ); + await generator(tree, options); + const config = readJson(tree, 'project.json'); + expect(config).toEqual({ + targets: { + 'local-registry': {}, + }, + }); + }); + + it('should install verdaccio to devDependencies', async () => { + await generator(tree, options); + const packageJson: PackageJson = readJson(tree, 'package.json'); + expect(packageJson.devDependencies).toEqual({ + verdaccio: '^5.0.4', + }); + }); +}); diff --git a/packages/js/src/generators/setup-verdaccio/generator.ts b/packages/js/src/generators/setup-verdaccio/generator.ts new file mode 100644 index 0000000000000..dd27e05ffd69f --- /dev/null +++ b/packages/js/src/generators/setup-verdaccio/generator.ts @@ -0,0 +1,65 @@ +import { + addDependenciesToPackageJson, + addProjectConfiguration, + convertNxGenerator, + formatFiles, + generateFiles, + ProjectConfiguration, + readJson, + TargetConfiguration, + Tree, + updateJson, +} from '@nx/devkit'; +import * as path from 'path'; +import { SetupVerdaccioGeneratorSchema } from './schema'; +import { verdaccioVersion } from '../../utils/versions'; + +export async function setupVerdaccio( + tree: Tree, + options: SetupVerdaccioGeneratorSchema +) { + if (!tree.exists('.verdaccio/config.yml')) { + generateFiles(tree, path.join(__dirname, 'files'), '.verdaccio', {}); + } + + const verdaccioTarget: TargetConfiguration = { + executor: '@nx/js:verdaccio', + options: { + port: 4873, + config: '.verdaccio/config.yml', + storage: 'tmp/local-registry/storage', + }, + }; + if (!tree.exists('project.json')) { + const { name } = readJson(tree, 'package.json'); + addProjectConfiguration(tree, name, { + root: '.', + targets: { + ['local-registry']: verdaccioTarget, + }, + }); + } else { + // use updateJson instead of updateProjectConfiguration due to unknown project name + updateJson(tree, 'project.json', (json: ProjectConfiguration) => { + if (!json.targets) { + json.targets = {}; + } + json.targets['local-registry'] ??= verdaccioTarget; + + return json; + }); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return addDependenciesToPackageJson( + tree, + {}, + { verdaccio: verdaccioVersion } + ); +} + +export default setupVerdaccio; +export const setupVerdaccioSchematic = convertNxGenerator(setupVerdaccio); diff --git a/packages/js/src/generators/setup-verdaccio/schema.d.ts b/packages/js/src/generators/setup-verdaccio/schema.d.ts new file mode 100644 index 0000000000000..008ea1fd43e23 --- /dev/null +++ b/packages/js/src/generators/setup-verdaccio/schema.d.ts @@ -0,0 +1,3 @@ +export interface SetupVerdaccioGeneratorSchema { + skipFormat: boolean; +} diff --git a/packages/js/src/generators/setup-verdaccio/schema.json b/packages/js/src/generators/setup-verdaccio/schema.json new file mode 100644 index 0000000000000..9efc84a8ef517 --- /dev/null +++ b/packages/js/src/generators/setup-verdaccio/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "SetupVerdaccio", + "title": "Setup Verdaccio", + "description": "Setup Verdaccio local-registry.", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + }, + "required": [] +} diff --git a/packages/js/src/utils/versions.ts b/packages/js/src/utils/versions.ts index bf0291a4c392b..ac29ce97ddde7 100644 --- a/packages/js/src/utils/versions.ts +++ b/packages/js/src/utils/versions.ts @@ -9,3 +9,4 @@ export const swcNodeVersion = '~1.4.2'; export const tsLibVersion = '^2.3.0'; export const typesNodeVersion = '18.7.1'; export const typescriptVersion = '~5.0.2'; +export const verdaccioVersion = '^5.0.4';