From 4f304f2151b04d808f48ee42d5d26b3bd8848862 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:57:36 +0200 Subject: [PATCH] refactor: structure verdaccio environment (#805) --- .../create-nodes-plugin.e2e.test.ts.snap | 76 ------- .../plugin-create-nodes.e2e.test.ts.snap | 43 ++++ ...n.e2e.test.ts => executor-cli.e2e.test.ts} | 14 +- .../tests/generator-configuration.e2e.test.ts | 30 +-- .../tests/generator-init.e2e.test.ts | 27 +-- ...est.ts => plugin-create-nodes.e2e.test.ts} | 57 ++--- e2e/nx-plugin-e2e/vite.config.e2e.ts | 2 +- global-setup.e2e.ts | 18 +- global-setup.verdaccio.ts | 42 ++++ package-lock.json | 11 + package.json | 1 + tools/src/npm/npm.plugin.ts | 7 +- tools/src/npm/utils.ts | 138 ++++-------- tools/src/publish/bin/publish-package.ts | 32 +-- tools/src/publish/publish.plugin.ts | 7 +- tools/src/publish/types.ts | 2 + tools/src/publish/utils.ts | 37 ++-- tools/src/utils.ts | 43 +++- .../src/verdaccio/bin/create-verdaccio-env.ts | 19 ++ tools/src/verdaccio/constants.ts | 7 + tools/src/verdaccio/env.ts | 172 +++++++++++++++ tools/src/verdaccio/registry.ts | 201 ++++++++++++++++++ tools/src/verdaccio/types.ts | 19 -- tools/src/verdaccio/verdaccio.plugin.ts | 12 +- 24 files changed, 690 insertions(+), 327 deletions(-) delete mode 100644 e2e/nx-plugin-e2e/tests/__snapshots__/create-nodes-plugin.e2e.test.ts.snap create mode 100644 e2e/nx-plugin-e2e/tests/__snapshots__/plugin-create-nodes.e2e.test.ts.snap rename e2e/nx-plugin-e2e/tests/{executor-autorun.e2e.test.ts => executor-cli.e2e.test.ts} (87%) rename e2e/nx-plugin-e2e/tests/{create-nodes-plugin.e2e.test.ts => plugin-create-nodes.e2e.test.ts} (81%) create mode 100644 global-setup.verdaccio.ts create mode 100644 tools/src/verdaccio/bin/create-verdaccio-env.ts create mode 100644 tools/src/verdaccio/constants.ts create mode 100644 tools/src/verdaccio/env.ts create mode 100644 tools/src/verdaccio/registry.ts delete mode 100644 tools/src/verdaccio/types.ts diff --git a/e2e/nx-plugin-e2e/tests/__snapshots__/create-nodes-plugin.e2e.test.ts.snap b/e2e/nx-plugin-e2e/tests/__snapshots__/create-nodes-plugin.e2e.test.ts.snap deleted file mode 100644 index 4de0a61b4..000000000 --- a/e2e/nx-plugin-e2e/tests/__snapshots__/create-nodes-plugin.e2e.test.ts.snap +++ /dev/null @@ -1,76 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`nx-plugin > should NOT add config targets dynamically if the project is configured 1`] = ` -{ - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "implicitDependencies": [], - "name": "my-lib", - "projectType": "library", - "root": "libs/my-lib", - "sourceRoot": "libs/my-lib/src", - "tags": [ - "scope:plugin", - ], - "targets": { - "code-pushup": { - "configurations": {}, - "executor": "@code-pushup/nx-plugin:autorun", - "options": {}, - }, - }, -} -`; - -exports[`nx-plugin > should add configuration target dynamically 1`] = ` -{ - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "implicitDependencies": [], - "name": "my-lib", - "projectType": "library", - "root": "libs/my-lib", - "sourceRoot": "libs/my-lib/src", - "tags": [ - "scope:plugin", - ], - "targets": { - "code-pushup--configuration": { - "configurations": {}, - "executor": "nx:run-commands", - "options": { - "command": "nx g @code-pushup/nx-plugin:configuration --skipTarget --targetName="code-pushup" --project="my-lib"", - }, - }, - }, -} -`; - -exports[`nx-plugin > should add executor target dynamically if the project is configured 1`] = ` -{ - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "implicitDependencies": [], - "name": "my-lib", - "projectType": "library", - "root": "libs/my-lib", - "sourceRoot": "libs/my-lib/src", - "tags": [ - "scope:plugin", - ], - "targets": { - "code-pushup": { - "configurations": {}, - "executor": "@code-pushup/nx-plugin:autorun", - "options": {}, - }, - }, -} -`; - -exports[`nx-plugin > should execute dynamic configuration target 1`] = ` -"import type { CoreConfig } from '@code-pushup/models'; - -// see: https://github.com/code-pushup/cli/blob/main/packages/models/docs/models-reference.md#coreconfig -export default { - plugins: [], -} satisfies CoreConfig; -" -`; diff --git a/e2e/nx-plugin-e2e/tests/__snapshots__/plugin-create-nodes.e2e.test.ts.snap b/e2e/nx-plugin-e2e/tests/__snapshots__/plugin-create-nodes.e2e.test.ts.snap new file mode 100644 index 000000000..158dc132d --- /dev/null +++ b/e2e/nx-plugin-e2e/tests/__snapshots__/plugin-create-nodes.e2e.test.ts.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`nx-plugin > should NOT add config targets dynamically if the project is configured 1`] = ` +{ + "code-pushup": { + "configurations": {}, + "executor": "@code-pushup/nx-plugin:autorun", + "options": {}, + }, +} +`; + +exports[`nx-plugin > should add configuration target dynamically 1`] = ` +{ + "code-pushup--configuration": { + "configurations": {}, + "executor": "nx:run-commands", + "options": { + "command": "nx g @code-pushup/nx-plugin:configuration --skipTarget --targetName="code-pushup" --project="my-lib"", + }, + }, +} +`; + +exports[`nx-plugin > should add executor target dynamically if the project is configured 1`] = ` +{ + "code-pushup": { + "configurations": {}, + "executor": "@code-pushup/nx-plugin:autorun", + "options": {}, + }, +} +`; + +exports[`nx-plugin > should execute dynamic configuration target 1`] = ` +"import type { CoreConfig } from '@code-pushup/models'; + +// see: https://github.com/code-pushup/cli/blob/main/packages/models/docs/models-reference.md#coreconfig +export default { + plugins: [], +} satisfies CoreConfig; +" +`; diff --git a/e2e/nx-plugin-e2e/tests/executor-autorun.e2e.test.ts b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts similarity index 87% rename from e2e/nx-plugin-e2e/tests/executor-autorun.e2e.test.ts rename to e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts index c9839d5fa..5ab13bcd2 100644 --- a/e2e/nx-plugin-e2e/tests/executor-autorun.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts @@ -27,22 +27,16 @@ async function addTargetToWorkspace( targets: { ...projectCfg.targets, ['code-pushup']: { - executor: `${join( - relativePathToCwd(cwd), - 'dist/packages/nx-plugin', - )}:autorun`, + executor: '@code-pushup/nx-plugin:autorun', }, }, }); const { root } = projectCfg; generateCodePushupConfig(tree, root, { - fileImports: `import type {CoreConfig} from "${join( - relativePathToCwd(cwd), - pathRelativeToPackage, - 'dist/packages/models', - )}";`, + fileImports: `import type {CoreConfig} from "@code-pushup/models";`, plugins: [ { + // @TODO replace with inline plugin fileImports: `import {customPlugin} from "${join( relativePathToCwd(cwd), pathRelativeToPackage, @@ -58,7 +52,7 @@ async function addTargetToWorkspace( describe('executor autorun', () => { let tree: Tree; const project = 'my-lib'; - const baseDir = 'tmp/nx-plugin-e2e/executor'; + const baseDir = 'tmp/e2e/nx-plugin-e2e/__test__/executor/cli'; beforeEach(async () => { tree = await generateWorkspaceAndProject(project); diff --git a/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts b/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts index 845323ae9..af84fba28 100644 --- a/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts @@ -1,6 +1,6 @@ import type { Tree } from '@nx/devkit'; import { readFile, rm } from 'node:fs/promises'; -import { join, relative } from 'node:path'; +import { join } from 'node:path'; import { afterEach, expect } from 'vitest'; import { generateCodePushupConfig } from '@code-pushup/nx-plugin'; import { @@ -10,19 +10,11 @@ import { import { removeColorCodes } from '@code-pushup/test-utils'; import { executeProcess } from '@code-pushup/utils'; -// @TODO replace with default bin after https://github.com/code-pushup/cli/issues/643 -function relativePathToDist(testDir: string): string { - return relative( - join(process.cwd(), testDir), - join(process.cwd(), 'dist/packages/nx-plugin'), - ); -} - describe('nx-plugin g configuration', () => { let tree: Tree; const project = 'my-lib'; const projectRoot = join('libs', project); - const baseDir = 'tmp/nx-plugin-e2e/generators/configuration'; + const baseDir = 'tmp/e2e/nx-plugin-e2e/__test__/generators/configuration'; beforeEach(async () => { tree = await generateWorkspaceAndProject(project); @@ -41,7 +33,7 @@ describe('nx-plugin g configuration', () => { args: [ 'nx', 'g', - `${relativePathToDist(cwd)}:configuration `, + '@code-pushup/nx-plugin:configuration', project, '--targetName=code-pushup', ], @@ -58,7 +50,7 @@ describe('nx-plugin g configuration', () => { const cleanedStdout = removeColorCodes(stdout); expect(cleanedStdout).toContain( - `NX Generating ${relativePathToDist(cwd)}:configuration`, + 'NX Generating @code-pushup/nx-plugin:configuration', ); expect(cleanedStdout).toMatch(/^CREATE.*code-pushup.config.ts/m); expect(cleanedStdout).toMatch(/^UPDATE.*project.json/m); @@ -89,7 +81,7 @@ describe('nx-plugin g configuration', () => { const { code, stdout, stderr } = await executeProcess({ command: 'npx', - args: ['nx', 'g', `${relativePathToDist(cwd)}:configuration `, project], + args: ['nx', 'g', '@code-pushup/nx-plugin:configuration', project], cwd, }); @@ -102,7 +94,7 @@ describe('nx-plugin g configuration', () => { const cleanedStdout = removeColorCodes(stdout); expect(cleanedStdout).toContain( - `NX Generating ${relativePathToDist(cwd)}:configuration`, + 'NX Generating @code-pushup/nx-plugin:configuration', ); expect(cleanedStdout).not.toMatch(/^CREATE.*code-pushup.config.ts/m); expect(cleanedStdout).toMatch(/^UPDATE.*project.json/m); @@ -131,7 +123,7 @@ describe('nx-plugin g configuration', () => { args: [ 'nx', 'g', - `${relativePathToDist(cwd)}:configuration `, + '@code-pushup/nx-plugin:configuration', project, '--skipConfig', ], @@ -143,7 +135,7 @@ describe('nx-plugin g configuration', () => { const cleanedStdout = removeColorCodes(stdout); expect(cleanedStdout).toContain( - `NX Generating ${relativePathToDist(cwd)}:configuration`, + 'NX Generating @code-pushup/nx-plugin:configuration', ); expect(cleanedStdout).not.toMatch(/^CREATE.*code-pushup.config.ts/m); expect(cleanedStdout).toMatch(/^UPDATE.*project.json/m); @@ -176,7 +168,7 @@ describe('nx-plugin g configuration', () => { args: [ 'nx', 'g', - `${relativePathToDist(cwd)}:configuration `, + '@code-pushup/nx-plugin:configuration', project, '--skipTarget', ], @@ -187,7 +179,7 @@ describe('nx-plugin g configuration', () => { const cleanedStdout = removeColorCodes(stdout); expect(cleanedStdout).toContain( - `NX Generating ${relativePathToDist(cwd)}:configuration`, + 'NX Generating @code-pushup/nx-plugin:configuration', ); expect(cleanedStdout).toMatch(/^CREATE.*code-pushup.config.ts/m); expect(cleanedStdout).not.toMatch(/^UPDATE.*project.json/m); @@ -220,7 +212,7 @@ describe('nx-plugin g configuration', () => { args: [ 'nx', 'g', - `${relativePathToDist(cwd)}:configuration `, + '@code-pushup/nx-plugin:configuration', project, '--dryRun', ], diff --git a/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts b/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts index 85bfc94f4..e30c9deae 100644 --- a/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts @@ -1,6 +1,6 @@ import type { Tree } from '@nx/devkit'; import { readFile, rm } from 'node:fs/promises'; -import { join, relative } from 'node:path'; +import { join } from 'node:path'; import { afterEach, expect } from 'vitest'; import { generateWorkspaceAndProject, @@ -9,17 +9,10 @@ import { import { removeColorCodes } from '@code-pushup/test-utils'; import { executeProcess } from '@code-pushup/utils'; -function relativePathToDist(testDir: string): string { - return relative( - join(process.cwd(), testDir), - join(process.cwd(), 'dist/packages/nx-plugin'), - ); -} - describe('nx-plugin g init', () => { let tree: Tree; const project = 'my-lib'; - const baseDir = 'tmp/nx-plugin-e2e/generators/init'; + const baseDir = 'tmp/e2e/nx-plugin-e2e/__test__/generators/init'; beforeEach(async () => { tree = await generateWorkspaceAndProject(project); @@ -35,13 +28,7 @@ describe('nx-plugin g init', () => { const { stderr } = await executeProcess({ command: 'npx', - args: [ - 'nx', - 'g', - `${relativePathToDist(cwd)}:init `, - project, - '--dryRun', - ], + args: ['nx', 'g', '@code-pushup/nx-plugin:init', project, '--dryRun'], cwd, }); @@ -60,7 +47,7 @@ describe('nx-plugin g init', () => { args: [ 'nx', 'g', - `${relativePathToDist(cwd)}:init `, + '@code-pushup/nx-plugin:init', project, '--skipInstall', ], @@ -70,7 +57,7 @@ describe('nx-plugin g init', () => { expect(code).toBe(0); const cleanedStdout = removeColorCodes(stdout); expect(cleanedStdout).toContain( - `NX Generating ${relativePathToDist(cwd)}:init`, + 'NX Generating @code-pushup/nx-plugin:init', ); expect(cleanedStdout).toMatch(/^UPDATE package.json/m); expect(cleanedStdout).toMatch(/^UPDATE nx.json/m); @@ -109,7 +96,7 @@ describe('nx-plugin g init', () => { args: [ 'nx', 'g', - `${relativePathToDist(cwd)}:init`, + '@code-pushup/nx-plugin:init', project, '--skipInstall', '--skipPackageJson', @@ -120,7 +107,7 @@ describe('nx-plugin g init', () => { expect(code).toBe(0); const cleanedStdout = removeColorCodes(stdout); expect(cleanedStdout).toContain( - `NX Generating ${relativePathToDist(cwd)}:init`, + 'NX Generating @code-pushup/nx-plugin:init', ); expect(cleanedStdout).not.toMatch(/^UPDATE package.json/m); expect(cleanedStdout).toMatch(/^UPDATE nx.json/m); diff --git a/e2e/nx-plugin-e2e/tests/create-nodes-plugin.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts similarity index 81% rename from e2e/nx-plugin-e2e/tests/create-nodes-plugin.e2e.test.ts rename to e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts index 892862851..855c701f0 100644 --- a/e2e/nx-plugin-e2e/tests/create-nodes-plugin.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts @@ -13,16 +13,11 @@ import { import { removeColorCodes } from '@code-pushup/test-utils'; import { executeProcess, readTextFile } from '@code-pushup/utils'; -// @TODO replace with default bin after https://github.com/code-pushup/cli/issues/643 -export function relativePathToCwd(testDir: string): string { - return relative(join(process.cwd(), testDir), process.cwd()); -} - describe('nx-plugin', () => { let tree: Tree; const project = 'my-lib'; const projectRoot = join('libs', project); - const baseDir = 'tmp/nx-plugin-e2e/plugin'; + const baseDir = 'tmp/e2e/nx-plugin-e2e/__test__/plugin/create-nodes'; beforeEach(async () => { tree = await generateWorkspaceAndProject(project); @@ -34,10 +29,7 @@ describe('nx-plugin', () => { it('should add configuration target dynamically', async () => { const cwd = join(baseDir, 'add-configuration-dynamically'); - registerPluginInWorkspace( - tree, - join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - ); + registerPluginInWorkspace(tree, '@code-pushup/nx-plugin'); await materializeTree(tree, cwd); const { code, projectJson } = await nxShowProjectJson(cwd, project); @@ -45,7 +37,7 @@ describe('nx-plugin', () => { expect(projectJson.targets).toStrictEqual({ ['code-pushup--configuration']: { - configurations: {}, // @TODO understand why this appears. should not be here + configurations: {}, executor: 'nx:run-commands', options: { command: `nx g @code-pushup/nx-plugin:configuration --skipTarget --targetName="code-pushup" --project="${project}"`, @@ -53,16 +45,13 @@ describe('nx-plugin', () => { }, }); - expect(projectJson).toMatchSnapshot(); + expect(projectJson.targets).toMatchSnapshot(); }); it('should execute dynamic configuration target', async () => { const cwd = join(baseDir, 'execute-dynamic-configuration'); registerPluginInWorkspace(tree, { - plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - options: { - bin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - }, + plugin: '@code-pushup/nx-plugin', }); await materializeTree(tree, cwd); @@ -86,7 +75,7 @@ describe('nx-plugin', () => { it('should consider plugin option targetName in configuration target', async () => { const cwd = join(baseDir, 'configuration-option-target-name'); registerPluginInWorkspace(tree, { - plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), + plugin: '@code-pushup/nx-plugin', options: { targetName: 'cp', }, @@ -105,7 +94,7 @@ describe('nx-plugin', () => { it('should consider plugin option bin in configuration target', async () => { const cwd = join(baseDir, 'configuration-option-bin'); registerPluginInWorkspace(tree, { - plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), + plugin: '@code-pushup/nx-plugin', options: { bin: 'XYZ', }, @@ -127,10 +116,7 @@ describe('nx-plugin', () => { it('should NOT add config targets dynamically if the project is configured', async () => { const cwd = join(baseDir, 'configuration-already-configured'); - registerPluginInWorkspace( - tree, - join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - ); + registerPluginInWorkspace(tree, '@code-pushup/nx-plugin'); const { root } = readProjectConfiguration(tree, project); generateCodePushupConfig(tree, root); await materializeTree(tree, cwd); @@ -144,15 +130,12 @@ describe('nx-plugin', () => { ['code-pushup--configuration']: expect.any(Object), }), ); - expect(projectJson).toMatchSnapshot(); + expect(projectJson.targets).toMatchSnapshot(); }); it('should add executor target dynamically if the project is configured', async () => { const cwd = join(baseDir, 'add-executor-dynamically'); - registerPluginInWorkspace( - tree, - join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - ); + registerPluginInWorkspace(tree, '@code-pushup/nx-plugin'); const { root } = readProjectConfiguration(tree, project); generateCodePushupConfig(tree, root); await materializeTree(tree, cwd); @@ -168,29 +151,23 @@ describe('nx-plugin', () => { }, }); - expect(projectJson).toMatchSnapshot(); + expect(projectJson.targets).toMatchSnapshot(); }); it('should execute dynamic executor target', async () => { const cwd = join(baseDir, 'execute-dynamic-executor'); const pathRelativeToPackage = relative(join(cwd, 'libs', project), cwd); registerPluginInWorkspace(tree, { - plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - options: { - bin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), - }, + plugin: '@code-pushup/nx-plugin', }); const { root } = readProjectConfiguration(tree, project); generateCodePushupConfig(tree, root, { - fileImports: `import type {CoreConfig} from "${join( - relativePathToCwd(cwd), - pathRelativeToPackage, - 'dist/packages/models', - )}";`, + fileImports: `import type {CoreConfig} from "@code-pushup/models";`, plugins: [ { + // @TODO replace with inline plugin fileImports: `import {customPlugin} from "${join( - relativePathToCwd(cwd), + relative(join(process.cwd(), cwd), process.cwd()), pathRelativeToPackage, 'dist/testing/test-utils', )}";`, @@ -216,7 +193,7 @@ describe('nx-plugin', () => { it('should consider plugin option bin in executor target', async () => { const cwd = join(baseDir, 'configuration-option-bin'); registerPluginInWorkspace(tree, { - plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), + plugin: '@code-pushup/nx-plugin', options: { bin: 'XYZ', }, @@ -239,7 +216,7 @@ describe('nx-plugin', () => { it('should consider plugin option projectPrefix in executor target', async () => { const cwd = join(baseDir, 'configuration-option-bin'); registerPluginInWorkspace(tree, { - plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), + plugin: '@code-pushup/nx-plugin', options: { projectPrefix: 'cli', }, diff --git a/e2e/nx-plugin-e2e/vite.config.e2e.ts b/e2e/nx-plugin-e2e/vite.config.e2e.ts index d27d15036..778d67ef9 100644 --- a/e2e/nx-plugin-e2e/vite.config.e2e.ts +++ b/e2e/nx-plugin-e2e/vite.config.e2e.ts @@ -16,7 +16,7 @@ export default defineConfig({ }, environment: 'node', include: ['tests/**/*.e2e.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - globalSetup: ['../../global-setup.ts'], + globalSetup: ['../../global-setup.verdaccio.ts'], setupFiles: ['../../testing/test-setup/src/lib/reset.mocks.ts'], }, }); diff --git a/global-setup.e2e.ts b/global-setup.e2e.ts index 5de6d5eca..e44f9f82b 100644 --- a/global-setup.e2e.ts +++ b/global-setup.e2e.ts @@ -6,9 +6,11 @@ import { nxRunManyNpmUninstall, } from './tools/src/npm/utils'; import { findLatestVersion, nxRunManyPublish } from './tools/src/publish/utils'; -import startLocalRegistry from './tools/src/verdaccio/start-local-registry'; +import { START_VERDACCIO_SERVER_TARGET_NAME } from './tools/src/verdaccio/constants'; +import startLocalRegistry, { + RegistryResult, +} from './tools/src/verdaccio/start-local-registry'; import stopLocalRegistry from './tools/src/verdaccio/stop-local-registry'; -import { RegistryResult } from './tools/src/verdaccio/types'; import { uniquePort } from './tools/src/verdaccio/utils'; const e2eDir = join('tmp', 'e2e'); @@ -22,7 +24,7 @@ export async function setup() { try { activeRegistry = await startLocalRegistry({ - localRegistryTarget: '@code-pushup/cli-source:start-verdaccio', + localRegistryTarget: `@code-pushup/cli-source:${START_VERDACCIO_SERVER_TARGET_NAME}`, storage: join(uniqueDir, 'storage'), port: uniquePort(), }); @@ -35,7 +37,11 @@ export async function setup() { const { registry } = activeRegistry.registryData; try { console.info('Publish packages'); - nxRunManyPublish({ registry, nextVersion: findLatestVersion() }); + nxRunManyPublish({ + registry, + nextVersion: findLatestVersion(), + parallel: 1, + }); } catch (error) { console.error('Error publishing packages:\n' + error.message); throw error; @@ -44,7 +50,7 @@ export async function setup() { // package install try { console.info('Installing packages'); - nxRunManyNpmInstall({ registry }); + nxRunManyNpmInstall({ registry, parallel: 1 }); } catch (error) { console.error('Error installing packages:\n' + error.message); throw error; @@ -56,7 +62,7 @@ export async function teardown() { const { stop } = activeRegistry; stopLocalRegistry(stop); - nxRunManyNpmUninstall(); + nxRunManyNpmUninstall({ parallel: 1 }); } await teardownTestFolder(e2eDir); } diff --git a/global-setup.verdaccio.ts b/global-setup.verdaccio.ts new file mode 100644 index 000000000..4985a3a93 --- /dev/null +++ b/global-setup.verdaccio.ts @@ -0,0 +1,42 @@ +import { bold, red } from 'ansis'; +import { setup as globalSetup } from './global-setup'; +import { nxRunManyNpmInstall } from './tools/src/npm/utils'; +import { findLatestVersion, nxRunManyPublish } from './tools/src/publish/utils'; +import { + VerdaccioEnvResult, + nxStartVerdaccioAndSetupEnv, + nxStopVerdaccioAndTeardownEnv, +} from './tools/src/verdaccio/env'; + +let activeRegistry: VerdaccioEnvResult; + +export async function setup() { + await globalSetup(); + + try { + activeRegistry = await nxStartVerdaccioAndSetupEnv({ + projectName: process.env['NX_TASK_TARGET_PROJECT'], + verbose: true, + }); + } catch (error) { + console.error('Error starting local verdaccio registry:\n' + error.message); + throw error; + } + + const { userconfig, workspaceRoot } = activeRegistry; + nxRunManyPublish({ + registry: activeRegistry.registry.url, + nextVersion: findLatestVersion(), + userconfig, + parallel: 1, + }); + nxRunManyNpmInstall({ prefix: workspaceRoot, userconfig, parallel: 1 }); +} + +export async function teardown() { + // potentially just skip as folder are deleted next line + // nxRunManyNpmUninstall({ userconfig, prefix: activeRegistry.workspaceRoot, parallel: 1 }); + + // comment out to see the folder and web interface + await nxStopVerdaccioAndTeardownEnv(activeRegistry); +} diff --git a/package-lock.json b/package-lock.json index 0f23184f5..662c5ae60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "@types/react-dom": "18.2.9", "@typescript-eslint/eslint-plugin": "6.13.2", "@typescript-eslint/parser": "6.13.2", + "@verdaccio/types": "^10.8.0", "@vitejs/plugin-react": "4.2.1", "@vitest/coverage-v8": "1.3.1", "@vitest/ui": "1.3.1", @@ -8717,6 +8718,16 @@ "streamx": "^2.15.0" } }, + "node_modules/@verdaccio/types": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/@verdaccio/types/-/types-10.8.0.tgz", + "integrity": "sha512-FuJyCRFPdy+gqCi0v29dE1xKn99Ztq6fuY9fb7ezeP1SRbUL/hgDaNkpjYvSIMCyb+dLFKOFBeZPyIUBLOSdlA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/verdaccio" + } + }, "node_modules/@verdaccio/ui-theme": { "version": "7.0.0-next-7.16", "resolved": "https://registry.npmjs.org/@verdaccio/ui-theme/-/ui-theme-7.0.0-next-7.16.tgz", diff --git a/package.json b/package.json index 10c15de1d..a32ab9ebd 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@types/react-dom": "18.2.9", "@typescript-eslint/eslint-plugin": "6.13.2", "@typescript-eslint/parser": "6.13.2", + "@verdaccio/types": "^10.8.0", "@vitejs/plugin-react": "4.2.1", "@vitest/coverage-v8": "1.3.1", "@vitest/ui": "1.3.1", diff --git a/tools/src/npm/npm.plugin.ts b/tools/src/npm/npm.plugin.ts index 24b265b3c..ad45ab8d4 100644 --- a/tools/src/npm/npm.plugin.ts +++ b/tools/src/npm/npm.plugin.ts @@ -5,7 +5,6 @@ import { } from '@nx/devkit'; import { dirname, join } from 'node:path'; import type { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json'; -import { someTargetsPresent } from '../utils'; import { NPM_CHECK_SCRIPT } from './constants'; type CreateNodesOptions = { @@ -24,7 +23,7 @@ export const createNodes: CreateNodes = [ ) => { const root = dirname(projectConfigurationFile); const projectConfiguration: ProjectConfiguration = readJsonFile( - projectConfigurationFile, + join(process.cwd(), projectConfigurationFile), ); const { publishableTags = 'publishable', @@ -68,10 +67,10 @@ function npmTargets({ }, }, 'npm-install': { - command: `npm install -D ${packageName}@{args.pkgVersion} --registry={args.registry}`, + command: `npm install -D ${packageName}@{args.pkgVersion} --prefix={args.prefix} --userconfig={args.userconfig}`, }, 'npm-uninstall': { - command: `npm uninstall ${packageName}`, + command: `npm uninstall ${packageName} --prefix={args.prefix} --userconfig={args.userconfig}`, }, }; } diff --git a/tools/src/npm/utils.ts b/tools/src/npm/utils.ts index db5c9dddc..593a655ec 100644 --- a/tools/src/npm/utils.ts +++ b/tools/src/npm/utils.ts @@ -1,109 +1,64 @@ -import { execFileSync, execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; import { objectToCliArgs } from '../../../packages/utils/src'; -import { removeColorCodes } from '../../../testing/test-utils/src'; -import { NPM_CHECK_SCRIPT } from './constants'; -import type { NpmCheckToken } from './types'; - -// @TODO The function is returning a strange string not matching the one in the function :) -export function npmCheck({ - pkgRange, - registry, - cwd, -}: { - pkgRange: string; - registry: string; - cwd?: string; -}): string | undefined { - const [foundPackage, token] = execSync( - `tsx ${NPM_CHECK_SCRIPT} ${objectToCliArgs({ - pkgRange, - registry, - }).join(' ')}`, - { cwd }, - ) - .toString() - .trim() - .split('#') as [string, NpmCheckToken]; - const cleanToken = token.trim(); - - if (cleanToken === 'FOUND') { - return cleanToken; - } else if (cleanToken === 'NOT_FOUND') { - return; - } else { - throw new Error( - `NPM check script returned invalid token ${cleanToken} for package ${foundPackage}`, - ); - } -} - -// @TODO The function is returning a strange string not matching the one in the function :) -export function nxNpmCheck({ - projectName, - registry, - cwd, - pkgVersion, -}: { - projectName?: string; - pkgVersion?: string; - registry?: string; - cwd?: string; -}) { - const [foundPackage, token] = execSync( - `nx npm-check ${projectName} ${objectToCliArgs({ - pkgVersion, - registry, - }).join(' ')}`, - { cwd }, - ) - .toString() - .trim() - .split('#') as [string, NpmCheckToken]; - const cleanToken = removeColorCodes(token); - - return cleanToken; - - if (cleanToken === 'FOUND') { - return token; - } else if (cleanToken === 'NOT_FOUND') { - return token; - } else { - throw new Error( - `Nx NPM check script returned invalid token ${cleanToken} for package ${foundPackage}`, - ); - } -} export type NpmInstallOptions = { + directory?: string; + prefix?: string; registry?: string; + userconfig?: string; tag?: string; pkgVersion?: string; + parallel?: number; }; export function nxRunManyNpmInstall({ registry, + prefix, + userconfig, tag = 'e2e', pkgVersion, + directory, + parallel, }: NpmInstallOptions) { - console.info(`Installing packages from registry: ${registry}.`); - - execFileSync( - 'npx', - [ - ...objectToCliArgs({ - _: ['nx', 'run-many'], - targets: 'npm-install', - parallel: 1, - ...(pkgVersion ? { pkgVersion } : {}), - ...(tag ? { tag } : {}), - ...(registry ? { registry } : {}), - }), - ], - { env: process.env, stdio: 'inherit', shell: true }, + console.info( + `Installing packages in ${directory} from registry: ${registry}.`, ); + try { + execFileSync( + 'nx', + [ + ...objectToCliArgs({ + _: ['run-many'], + targets: 'npm-install', + ...(parallel ? { parallel } : {}), + ...(pkgVersion ? { pkgVersion } : {}), + ...(tag ? { tag } : {}), + ...(registry ? { registry } : {}), + ...(userconfig ? { userconfig } : {}), + ...(prefix ? { prefix } : {}), + }), + ], + { + env: process.env, + stdio: 'inherit', + shell: true, + cwd: directory ?? process.cwd(), + }, + ); + } catch (error) { + console.error('Error installing packages:\n' + error.message); + throw error; + } } -export function nxRunManyNpmUninstall() { +export function nxRunManyNpmUninstall({ + parallel, + ...opt +}: { + prefix?: string; + userconfig?: string; + parallel?: number; +}) { console.info('Uninstalling all NPM packages.'); try { execFileSync( @@ -111,7 +66,8 @@ export function nxRunManyNpmUninstall() { objectToCliArgs({ _: ['nx', 'run-many'], targets: 'npm-uninstall', - parallel: 1, + parallel, + ...opt, }), { env: process.env, stdio: 'inherit', shell: true }, ); diff --git a/tools/src/publish/bin/publish-package.ts b/tools/src/publish/bin/publish-package.ts index 51056f6f2..eca0e7d57 100644 --- a/tools/src/publish/bin/publish-package.ts +++ b/tools/src/publish/bin/publish-package.ts @@ -8,7 +8,7 @@ */ import { execSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { join, relative } from 'node:path'; import { DEFAULT_REGISTRY } from 'verdaccio/build/lib/constants'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -24,6 +24,7 @@ const argv = yargs(hideBin(process.argv)) nextVersion: { type: 'string' }, tag: { type: 'string', default: 'next' }, registry: { type: 'string' }, + userconfig: { type: 'string' }, verbose: { type: 'boolean' }, }) .coerce('nextVersion', parseVersion).argv; @@ -33,6 +34,7 @@ const { nextVersion, tag, registry = DEFAULT_REGISTRY, + userconfig, verbose, } = argv as PublishOptions; const version = nextVersion ?? findLatestVersion(); @@ -40,19 +42,6 @@ const version = nextVersion ?? findLatestVersion(); // Updating the version in "package.json" before publishing nxBumpVersion({ nextVersion: version, directory, projectName }); -const packageJson = JSON.parse( - readFileSync(join(directory, 'package.json')).toString(), -); -const pkgRange = `${packageJson.name}@${version}`; - -// @TODO if we hav no registry set up this implementation swallows the error -/*if (npmCheck( - { registry, pkgRange }, -) === 'FOUND') { - console.warn(`Package ${version} is already published.`); - process.exit(0); -}*/ - try { execSync( objectToCliArgs({ @@ -60,6 +49,14 @@ try { access: 'public', ...(tag ? { tag } : {}), ...(registry ? { registry } : {}), + ...(userconfig + ? { + userconfig: relative( + join(process.cwd(), directory ?? ''), + join(process.cwd(), userconfig), + ), + } + : {}), }).join(' '), { cwd: directory, @@ -67,7 +64,7 @@ try { ); } catch (error) { if ( - error.message.includes( + (error as Error).message.includes( `need auth This command requires you to be logged in to ${registry}`, ) ) { @@ -75,7 +72,10 @@ try { `Authentication error! Check if your registry is set up correctly. If you publish to a public registry run login before.`, ); process.exit(1); - } else if (error.message.includes(`Cannot publish over existing version`)) { + } else if ( + error instanceof Error && + error.message.includes(`Cannot publish over existing version`) + ) { console.info(`Version ${version} already published to ${registry}.`); process.exit(0); } diff --git a/tools/src/publish/publish.plugin.ts b/tools/src/publish/publish.plugin.ts index 0562f0efb..34fcf1da0 100644 --- a/tools/src/publish/publish.plugin.ts +++ b/tools/src/publish/publish.plugin.ts @@ -3,10 +3,9 @@ import { type CreateNodesContext, readJsonFile, } from '@nx/devkit'; -import { dirname, join } from 'node:path'; +import { dirname } from 'node:path'; import type { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json'; -import { someTargetsPresent } from '../utils'; -import { BUMP_SCRIPT, LOGIN_CHECK_SCRIPT, PUBLISH_SCRIPT } from './constants'; +import { BUMP_SCRIPT, PUBLISH_SCRIPT } from './constants'; type CreateNodesOptions = { tsconfig?: string; @@ -75,7 +74,7 @@ function publishTargets({ return { publish: { dependsOn: ['build'], - command: `tsx --tsconfig={args.tsconfig} {args.script} --projectName=${projectName} --directory=${directory} --registry={args.registry} --nextVersion={args.nextVersion} --tag={args.tag} --verbose=${verbose}`, + command: `tsx --tsconfig={args.tsconfig} {args.script} --projectName=${projectName} --directory=${directory} --registry={args.registry} --userconfig={args.userconfig} --nextVersion={args.nextVersion} --tag={args.tag} --verbose=${verbose}`, options: { script: publishScript, tsconfig, diff --git a/tools/src/publish/types.ts b/tools/src/publish/types.ts index aa455ca24..c1c9d9426 100644 --- a/tools/src/publish/types.ts +++ b/tools/src/publish/types.ts @@ -1,10 +1,12 @@ export type PublishOptions = { projectName?: string; directory?: string; + userconfig?: string; registry?: string; tag?: string; nextVersion: string; verbose?: boolean; + parallel?: number; }; export type BumpOptions = { nextVersion: string; diff --git a/tools/src/publish/utils.ts b/tools/src/publish/utils.ts index e610e6af8..c2a84f08f 100644 --- a/tools/src/publish/utils.ts +++ b/tools/src/publish/utils.ts @@ -8,23 +8,30 @@ export function nxRunManyPublish({ registry, tag = 'e2e', nextVersion, + userconfig, + parallel, }: PublishOptions) { console.info(`Publish packages to registry: ${registry}.`); - - execFileSync( - 'npx', - [ - 'nx', - 'run-many', - '--targets=publish', - '--parallel=1', - '--', - ...(nextVersion ? [`--nextVersion=${nextVersion}`] : []), - ...(tag ? [`--tag=${tag}`] : []), - ...(registry ? [`--registry=${registry}`] : []), - ], - { env: process.env, stdio: 'inherit', shell: true }, - ); + try { + execFileSync( + 'npx', + [ + 'nx', + 'run-many', + '--targets=publish', + ...(parallel ? [`--parallel=${parallel}`] : []), + '--', + ...(nextVersion ? [`--nextVersion=${nextVersion}`] : []), + ...(tag ? [`--tag=${tag}`] : []), + ...(registry ? [`--registry=${registry}`] : []), + ...(userconfig ? [`--userconfig=${userconfig}`] : []), + ], + { env: process.env, stdio: 'inherit', shell: true }, + ); + } catch (error) { + console.error('Error publishing packages:\n' + error.message); + throw error; + } } export function findLatestVersion(): string { diff --git a/tools/src/utils.ts b/tools/src/utils.ts index cb47ee2ed..e8ccdb3ff 100644 --- a/tools/src/utils.ts +++ b/tools/src/utils.ts @@ -1,4 +1,8 @@ -import type { TargetConfiguration } from '@nx/devkit'; +import { + type ProjectGraph, + type TargetConfiguration, + readCachedProjectGraph, +} from '@nx/devkit'; export function someTargetsPresent( targets: Record, @@ -10,7 +14,7 @@ export function someTargetsPresent( return Object.keys(targets).some(target => searchTargets.includes(target)); } -export function invariant(condition, message) { +export function invariant(condition: string | boolean, message: string) { if (!condition) { console.error(message); process.exit(1); @@ -30,3 +34,38 @@ export function parseVersion(rawVersion: string) { return undefined; } } + +export async function getAllDependencies( + projectName: string, +): Promise { + // TODO check if the caching problems are introduced by readCachedProjectGraph + const projectGraph: ProjectGraph = await readCachedProjectGraph(); + + // Helper function to recursively collect dependencies + const collectDependencies = ( + project: string, + visited: Set = new Set(), + ): Set => { + // If the project has already been visited, return the accumulated set + if (visited.has(project)) { + return visited; + } + + // Add the current project to the visited set + const updatedVisited = new Set(visited).add(project); + + // Get the direct dependencies of the current project + const dependencies = projectGraph.dependencies[project] || []; + + // Recursively collect dependencies of all direct dependencies + return dependencies.reduce((acc, dependency) => { + return collectDependencies(dependency.target, acc); + }, updatedVisited); + }; + + // Get all dependencies, then remove the original project (optional) + const allDependencies = collectDependencies(projectName); + allDependencies.delete(projectName); + + return Array.from(allDependencies).filter(dep => !dep.startsWith('npm:')); +} diff --git a/tools/src/verdaccio/bin/create-verdaccio-env.ts b/tools/src/verdaccio/bin/create-verdaccio-env.ts new file mode 100644 index 000000000..4aa64d123 --- /dev/null +++ b/tools/src/verdaccio/bin/create-verdaccio-env.ts @@ -0,0 +1,19 @@ +import yargs, { Options } from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { + type StartVerdaccioAndSetupEnvOptions, + nxStartVerdaccioAndSetupEnv, +} from '../env'; + +const argv = yargs(hideBin(process.argv)) + .version(false) + .options({ + verbose: { type: 'boolean' }, + projectName: { type: 'string', demandOption: true }, + port: { type: 'string' }, + } satisfies Partial>).argv; + +(async () => { + await nxStartVerdaccioAndSetupEnv(argv as StartVerdaccioAndSetupEnvOptions); + process.exit(0); +})(); diff --git a/tools/src/verdaccio/constants.ts b/tools/src/verdaccio/constants.ts new file mode 100644 index 000000000..f1d75171f --- /dev/null +++ b/tools/src/verdaccio/constants.ts @@ -0,0 +1,7 @@ +import { join } from 'node:path'; + +export const START_VERDACCIO_SERVER_TARGET_NAME = 'start-verdaccio-server'; +export const START_VERDACCIO_ENV_TARGET_NAME = 'start-verdaccio-env'; +export const STOP_VERDACCIO_TARGET_NAME = 'stop-verdaccio'; +export const DEFAULT_VERDACCIO_STORAGE = join('tmp', 'verdaccio', 'storage'); +export const DEFAULT_VERDACCIO_CONFIG = join('.verdaccio', 'config.yml'); diff --git a/tools/src/verdaccio/env.ts b/tools/src/verdaccio/env.ts new file mode 100644 index 000000000..4f743e6e5 --- /dev/null +++ b/tools/src/verdaccio/env.ts @@ -0,0 +1,172 @@ +import { bold, gray, red } from 'ansis'; +// eslint-disable-next-line n/no-sync +import { execFileSync, execSync } from 'node:child_process'; +import { join } from 'node:path'; +// can't import from utils +import { objectToCliArgs } from '../../../packages/nx-plugin/src'; +import { + setupTestFolder, + teardownTestFolder, +} from '../../../testing/test-setup/src'; +import { ensureDirectoryExists } from '../../../testing/test-utils/src'; +import { + NxStarVerdaccioOptions, + Registry, + nxStartVerdaccioServer, +} from './registry'; + +export function projectE2eScope(projectName: string): string { + return join('tmp', 'e2e', projectName); +} + +export type VerdaccioEnv = { + workspaceRoot: string; + userconfig?: string; +}; + +export function configureRegistry( + { + url, + urlNoProtocol, + userconfig, + }: Registry & Pick, + verbose?: boolean, +) { + /** + * Protocol-Agnostic Configuration: The use of // allows NPM to configure authentication for a registry without tying it to a specific protocol (http: or https:). + * This is particularly useful when the registry might be accessible via both HTTP and HTTPS. + * + * Example: //registry.npmjs.org/:_authToken=your-token + */ + const token = 'secretVerdaccioToken'; + const setAuthToken = `npm config set ${urlNoProtocol}/:_authToken "${token}" ${objectToCliArgs( + { userconfig }, + ).join(' ')}`; + if (verbose) { + console.info( + `${gray('>')} ${gray(bold('Verdaccio-Env'))} Execute: ${setAuthToken}`, + ); + } + execSync(setAuthToken); + + const setRegistry = `npm config set registry="${url}" ${objectToCliArgs({ + userconfig, + }).join(' ')}`; + if (verbose) { + console.info( + `${gray('>')} ${gray(bold('Verdaccio-Env'))} Execute: ${userconfig}`, + ); + } + execSync(setRegistry); +} + +export function unconfigureRegistry( + { urlNoProtocol }: Pick, + verbose?: boolean, +) { + execSync(`npm config delete registry`); + execSync(`npm config delete ${urlNoProtocol}/:_authToken`); + if (verbose) { + console.info(`${gray('>')} ${gray(bold('Verdaccio-Env'))} delete registry`); + console.info( + `${gray('>')} ${gray( + bold('Verdaccio-Env'), + )} delete npm authToken: ${urlNoProtocol}`, + ); + } +} + +export async function setupNpmWorkspace(directory: string, verbose?: boolean) { + if (verbose) { + console.info( + `${gray('>')} ${gray( + bold('Verdaccio-Env'), + )} Execute: npm init in directory ${directory}`, + ); + } + const cwd = process.cwd(); + await ensureDirectoryExists(directory); + process.chdir(join(cwd, directory)); + try { + execFileSync('npm', ['init', '--force']).toString(); + } catch (error) { + console.error( + `${red('>')} ${red( + bold('Verdaccio-Env'), + )} Error creating NPM workspace: ${(error as Error).message}`, + ); + } finally { + process.chdir(cwd); + } +} + +export type StartVerdaccioAndSetupEnvOptions = Partial< + NxStarVerdaccioOptions & VerdaccioEnv +> & + Pick; + +export type VerdaccioEnvResult = VerdaccioEnv & { + registry: Registry; + stop: () => void; +}; + +export async function nxStartVerdaccioAndSetupEnv({ + projectName, + port, + verbose = false, + workspaceRoot = projectE2eScope(projectName), + location = 'none', + // reset or remove cached packages and/or metadata. + clear = true, +}: StartVerdaccioAndSetupEnvOptions): Promise { + // set up NPM workspace environment + const storage = join(workspaceRoot, 'storage'); + + // @TODO potentially done by verdaccio task when clearing storage + await setupTestFolder(storage); + const registryResult = await nxStartVerdaccioServer({ + projectName, + storage, + port, + location, + clear, + verbose, + }); + + await setupNpmWorkspace(workspaceRoot, verbose); + + const userconfig = join(workspaceRoot, '.npmrc'); + configureRegistry({ ...registryResult.registry, userconfig }, verbose); + + return { + ...registryResult, + stop: () => { + registryResult.stop(); + unconfigureRegistry(registryResult.registry, verbose); + }, + workspaceRoot, + userconfig, + } satisfies VerdaccioEnvResult; +} + +export async function nxStopVerdaccioAndTeardownEnv( + result: VerdaccioEnvResult, +) { + if (result) { + const { stop, registry, workspaceRoot } = result; + if (stop == null) { + throw new Error( + 'global e2e teardown script was not able to derive the stop script for the active registry from "activeRegistry"', + ); + } + console.info(`Un configure registry: ${registry.url}`); + if (typeof stop === 'function') { + stop(); + } else { + console.error('Stop is not a function. Type:', typeof stop); + } + await teardownTestFolder(workspaceRoot); + } else { + throw new Error(`Failed to stop registry.`); + } +} diff --git a/tools/src/verdaccio/registry.ts b/tools/src/verdaccio/registry.ts new file mode 100644 index 000000000..3afe3788f --- /dev/null +++ b/tools/src/verdaccio/registry.ts @@ -0,0 +1,201 @@ +import { bold, gray, red } from 'ansis'; +import { executeProcess } from '@code-pushup/utils'; +// can't import from utils +import { objectToCliArgs } from '../../../packages/nx-plugin'; +import { teardownTestFolder } from '../../../testing/test-setup/src'; +import { killProcesses, listProcess } from '../debug/utils'; +import { START_VERDACCIO_SERVER_TARGET_NAME } from './constants'; + +export function uniquePort(): number { + return Number((6000 + Number(Math.random() * 1000)).toFixed(0)); +} + +export type RegistryServer = { + protocol: string; + port: string | number; + host: string; + urlNoProtocol: string; + url: string; +}; +export type Registry = RegistryServer & + Required>; + +export type RegistryResult = { + registry: Registry; + stop: () => void; +}; + +export function parseRegistryData(stdout: string): RegistryServer { + const output = stdout.toString(); + + // Extract protocol, host, and port + const match = output.match( + /(?https?):\/\/(?[^:]+):(?\d+)/, + ); + + if (!match?.groups) { + throw new Error('Could not parse registry data from stdout'); + } + + const protocol = match.groups['proto']; + if (!protocol || !['http', 'https'].includes(protocol)) { + throw new Error( + `Invalid protocol ${protocol}. Only http and https are allowed.`, + ); + } + const host = match.groups['host']; + if (!host) { + throw new Error(`Invalid host ${String(host)}.`); + } + const port = !Number.isNaN(Number(match.groups['port'])) + ? Number(match.groups['port']) + : undefined; + if (!port) { + throw new Error(`Invalid port ${String(port)}.`); + } + return { + protocol, + host, + port, + urlNoProtocol: `//${host}:${port}`, + url: `${protocol}://${host}:${port}`, + }; +} + +export type NxStarVerdaccioOnlyOptions = { + projectName?: string; + verbose?: boolean; +}; + +export type VerdaccioExecuterOptions = { + storage?: string; + port?: string; + p?: string; + config?: string; + c?: string; + location: string; + // reset or remove cached packages and/or metadata. + clear: boolean; +}; + +export type NxStarVerdaccioOptions = VerdaccioExecuterOptions & + NxStarVerdaccioOnlyOptions; + +export async function nxStartVerdaccioServer({ + projectName = '', + storage, + port, + location, + clear, + verbose = false, +}: NxStarVerdaccioOptions): Promise { + let startDetected = false; + + const positionalArgs = [ + 'exec', + 'nx', + START_VERDACCIO_SERVER_TARGET_NAME, + projectName ?? '', + '--', + ]; + const args = objectToCliArgs< + Partial< + VerdaccioExecuterOptions & { _: string[]; verbose: boolean; cwd: string } + > + >({ + _: positionalArgs, + storage, + port, + verbose, + location, + clear, + }); + + // a link to the process started by this command, not one of the child processes. (every port is spawned by a command) + const commandId = positionalArgs.join(' '); + + if (verbose) { + console.info( + `${gray('>')} ${gray( + bold('Verdaccio'), + )} Start server with command: ${commandId}`, + ); + } + + return ( + new Promise((resolve, reject) => { + executeProcess({ + command: 'npm', + args, + shell: true, + observer: { + onStdout: (stdout: string) => { + if (verbose) { + process.stdout.write( + `${gray('>')} ${gray(bold('Verdaccio'))} ${stdout}`, + ); + } + + // Log of interest: warn --- http address - http://localhost:/ - verdaccio/5.31.1 + if (!startDetected && stdout.includes('http://localhost:')) { + // only setup env one time + startDetected = true; + + const result: RegistryResult = { + registry: { + storage, + ...parseRegistryData(stdout), + }, + // https://verdaccio.org/docs/cli/#default-database-file-location + stop: () => { + // this makes the process throw + killProcesses({ commandMatch: commandId }); + }, + }; + + console.info( + `${gray('>')} ${gray( + bold('Verdaccio'), + )} Registry started on URL: ${bold( + result.registry.url, + )}, with PID: ${bold( + listProcess({ commandMatch: commandId }).at(0)?.pid, + )}`, + ); + if (verbose) { + console.info(`${gray('>')} ${gray(bold('Verdaccio'))}`); + console.table(result); + } + + resolve(result); + } + }, + onStderr: (stderr: string) => { + if (verbose) { + process.stdout.write( + `${red('>')} ${red(bold('Verdaccio'))} ${stderr}`, + ); + } + }, + }, + }) + // @TODO reconsider this error handling + .catch(error => { + if (error.message !== 'Failed to start verdaccio: undefined') { + console.error( + `${red('>')} ${red( + bold('Verdaccio'), + )} Error starting ${projectName} verdaccio registry:\n${error}`, + ); + } else { + reject(error); + } + }); + }) + // in case the server dies unexpectedly clean folder + .catch((error: unknown) => { + teardownTestFolder(storage); + throw error; + }) + ); +} diff --git a/tools/src/verdaccio/types.ts b/tools/src/verdaccio/types.ts deleted file mode 100644 index 2731b57e9..000000000 --- a/tools/src/verdaccio/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type RegistryData = { - protocol: string; - port: string | number; - host: string; - registryNoProtocol: string; - registry: string; -}; - -export type RegistryOptions = { - // local registry target to run - localRegistryTarget: string; - // storage folder for the local registry - storage?: string; - verbose?: boolean; -}; -export type RegistryResult = { - registryData: RegistryData; - stop: () => void; -}; diff --git a/tools/src/verdaccio/verdaccio.plugin.ts b/tools/src/verdaccio/verdaccio.plugin.ts index b0f00e627..0c8e45685 100644 --- a/tools/src/verdaccio/verdaccio.plugin.ts +++ b/tools/src/verdaccio/verdaccio.plugin.ts @@ -5,6 +5,8 @@ import { } from '@nx/devkit'; import { dirname } from 'node:path'; import type { ProjectConfiguration } from 'nx/src/config/workspace-json-project-json'; +import { someTargetsPresent } from '../utils'; +import { START_VERDACCIO_SERVER_TARGET_NAME } from './constants'; import { uniquePort } from './utils'; type CreateNodesOptions = { @@ -35,8 +37,12 @@ export const createNodes: CreateNodes = [ projectConfigurationFile, ); + const hasPreVerdaccioTargets = someTargetsPresent( + projectConfiguration?.targets ?? {}, + preTargets, + ); const isRootProject = root === '.'; - if (!isRootProject) { + if (!hasPreVerdaccioTargets && !isRootProject) { return {}; } @@ -59,11 +65,9 @@ function verdaccioTargets({ port, config, storage, - preTargets, }: Required>) { - const targets = Array.isArray(preTargets) ? preTargets : [preTargets]; return { - 'start-verdaccio': { + [START_VERDACCIO_SERVER_TARGET_NAME]: { executor: '@nx/js:verdaccio', options: { port,