Open DannyvanHolten opened 2 weeks ago
I created a draft that I temporarily use locally as a "fork". I did not fix Yarn V2 support for this yet. And I'm not sure if this meets the required coding standards Nx demands. But if this is the correct direction I will take more effort to fix this for Yarn V2 (I might need some help) and do all the required steps:
import { ExecutorContext, logger } from '@nx/devkit';
import { existsSync, rmSync } from 'fs-extra';
import { ChildProcess, execSync, fork } from 'child_process';
import * as detectPort from 'detect-port';
import { join, resolve } from 'path';
import { VerdaccioExecutorSchema } from './schema';
import { major } from 'semver';
let childProcess: ChildProcess;
let env: NodeJS.ProcessEnv = {
SKIP_YARN_COREPACK_CHECK: 'true',
...process.env,
};
/**
* - 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.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 port = await detectPort(options.port);
if (port !== options.port) {
logger.info(`Port ${options.port} was occupied. Using port ${port}.`);
options.port = port;
}
const cleanupFunctions =
options.location === 'none' ? [] : [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, context.root);
} catch (e) {
logger.error('Failed to start verdaccio: ' + e?.toString());
return {
success: false,
port: options.port,
};
}
return {
success: true,
port: options.port,
};
}
/**
* Fork the verdaccio process: https://verdaccio.org/docs/verdaccio-programmatically/#using-fork-from-child_process-module
*/
function startVerdaccio(
options: VerdaccioExecutorSchema,
workspaceRoot: string
) {
return new Promise((resolve, reject) => {
childProcess = fork(
require.resolve('verdaccio/bin/verdaccio'),
createVerdaccioOptions(options, workspaceRoot),
{
env: {
...process.env,
VERDACCIO_HANDLE_KILL_SIGNALS: 'true',
...(options.storage
? { VERDACCIO_STORAGE_PATH: options.storage }
: {}),
},
stdio: 'inherit',
}
);
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,
workspaceRoot: string
) {
const verdaccioArgs: string[] = [];
if (options.config) {
verdaccioArgs.push('--config', join(workspaceRoot, options.config));
} else {
options.port ??= 4873; // set default port if config is not provided
}
if (options.port) {
verdaccioArgs.push('--listen', options.port.toString());
}
return verdaccioArgs;
}
function setupNpm(options: VerdaccioExecutorSchema) {
try {
execSync('npm --version', { env });
} catch (e) {
return () => {};
}
let npmRegistryPaths: string[] = [];
const scopes: string[] = ['default', ...(options.scopes || [])];
try {
scopes.forEach((scope) => {
const scopeName = scope !== 'default' ? `${scope}:` : '';
npmRegistryPaths = [
...npmRegistryPaths,
execSync(
`npm config get '${scopeName}registry' --location ${options.location}`,
{env}
)
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', '') // strip out ansi codes
];
execSync(
`npm config set '${scopeName}registry' http://localhost:${options.port}/ --location ${options.location}`,
{env}
);
logger.info(`Set npm ${scopeName}registry to http://localhost:${options.port}/`);
});
execSync(
`npm config set //localhost:${options.port}/:_authToken="secretVerdaccioToken" --location ${options.location}`,
{env}
);
} catch (e) {
throw new Error(
`Failed to set npm registry to http://localhost:${options.port}/: ${e.message}`
);
}
return () => {
try {
const currentNpmRegistryPath = execSync(
`npm config get registry --location ${options.location}`,
{ env }
)
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes
if (npmRegistryPaths.length > 0 && currentNpmRegistryPath.includes('localhost')) {
scopes.forEach((scope, index) => {
const scopeName = scope !== 'default' ? `${scope}:` : '';
execSync(
`npm config set '${scopeName}registry' ${npmRegistryPaths[index]} --location ${options.location}`,
{env}
);
logger.info(`Reset npm ${scopeName}registry to ${npmRegistryPaths[index]}`);
});
} else {
execSync(`npm config delete registry --location ${options.location}`, {
env,
});
logger.info('Cleared custom npm registry');
}
execSync(
`npm config delete //localhost:${options.port}/:_authToken --location ${options.location}`,
{ env }
);
} catch (e) {
throw new Error(`Failed to reset npm registry: ${e.message}`);
}
};
}
function getYarnUnsafeHttpWhitelist(isYarnV1: boolean) {
return !isYarnV1
? new Set<string>(
JSON.parse(
execSync(`yarn config get unsafeHttpWhitelist --json`, {
env,
}).toString()
)
)
: null;
}
function setYarnUnsafeHttpWhitelist(
currentWhitelist: Set<string>,
options: VerdaccioExecutorSchema
) {
if (currentWhitelist.size > 0) {
execSync(
`yarn config set unsafeHttpWhitelist --json '${JSON.stringify(
Array.from(currentWhitelist)
)}'` + (options.location === 'user' ? ' --home' : ''),
{ env }
);
} else {
execSync(
`yarn config unset unsafeHttpWhitelist` +
(options.location === 'user' ? ' --home' : ''),
{ env }
);
}
}
function setupYarn(options: VerdaccioExecutorSchema) {
let isYarnV1;
let yarnRegistryPaths: string[] = [];
const scopes: string[] = ['default', ...(options.scopes || [])];
try {
isYarnV1 =
major(execSync('yarn --version', { env }).toString().trim()) === 1;
} catch {
// This would fail if yarn is not installed which is okay
return () => {};
}
try {
const registryConfigName = isYarnV1 ? 'registry' : 'npmRegistryServer';
scopes.forEach((scope, index) => {
const scopeName = scope !== 'default' ? `${scope}:` : '';
yarnRegistryPaths = [
...yarnRegistryPaths,
execSync(`yarn config get ${scopeName}${registryConfigName}`, {
env,
})
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', '') // strip out ansi codes
];
execSync(
`yarn config set ${scopeName}${registryConfigName} http://localhost:${options.port}/` +
(options.location === 'user' ? ' --home' : ''),
{env}
);
logger.info(`Set yarn ${scopeName}registry to http://localhost:${options.port}/`);
});
const currentWhitelist: Set<string> | null =
getYarnUnsafeHttpWhitelist(isYarnV1);
let whitelistedLocalhost = false;
if (!isYarnV1 && !currentWhitelist.has('localhost')) {
whitelistedLocalhost = true;
currentWhitelist.add('localhost');
setYarnUnsafeHttpWhitelist(currentWhitelist, options);
logger.info(
`Whitelisted http://localhost:${options.port}/ as an unsafe http server`
);
}
return () => {
try {
const currentYarnRegistryPath = execSync(
`yarn config get ${registryConfigName}`,
{ env }
)
?.toString()
?.trim()
?.replace('\u001b[2K\u001b[1G', ''); // strip out ansi codes
if (yarnRegistryPaths.length > 0 && currentYarnRegistryPath.includes('localhost')) {
scopes.forEach((scope, index) => {
const scopeName = scope !== 'default' ? `${scope}:` : '';
execSync(
`yarn config set ${scopeName}${registryConfigName} ${yarnRegistryPaths[index]}` +
(options.location === 'user' ? ' --home' : ''),
{env}
);
logger.info(
`Reset yarn ${scopeName}${registryConfigName} to ${yarnRegistryPaths[index]}`
);
});
} else {
execSync(
`yarn config ${
isYarnV1 ? 'delete' : 'unset'
} ${registryConfigName}` +
(options.location === 'user' ? ' --home' : ''),
{ env }
);
logger.info(`Cleared custom yarn ${registryConfigName}`);
}
if (whitelistedLocalhost) {
const currentWhitelist: Set<string> =
getYarnUnsafeHttpWhitelist(isYarnV1);
if (currentWhitelist.has('localhost')) {
currentWhitelist.delete('localhost');
setYarnUnsafeHttpWhitelist(currentWhitelist, options);
logger.info(
`Removed http://localhost:${options.port}/ as an unsafe http server`
);
}
}
} catch (e) {
throw new Error(`Failed to reset yarn registry: ${e.message}`);
}
};
} catch (e) {
throw new Error(
`Failed to set yarn registry to http://localhost:${options.port}/: ${e.message}`
);
}
}
export default verdaccioExecutor;
This seems feasible / fine. We'd definitely appreciate the contribution. I'm going to assign you as well as @xiongemi on our side, she can help guide you if questions come up.
Good to hear. I will probably have time for this upcoming Wednesday/Thursday and I will make an MR. I'll probably need some time to get this repository up and running, but that will be fine. I'll contact xiongemi when I made an MR :)
@DannyvanHolten @AgentEnder actually i created a pr for add the scopes options https://github.com/nrwl/nx/pull/26918
Current Behavior
I currently have a .npmrc file with a private repository pointing to gitlab for internal packages.
This is because we want to distribute the packages with
@private
scope only to be published to our private registry.However, we do want to be able to also test these scoped packages with Verdaccio locally.
The current implementation for
@nx/js:verdaccio
has no option for scopes. as seen in the implementation:Expected Behavior
I would like to be able to provide scopes into the project.json configuration. like so:
I believe if we create a loop in the implementation and add the scoped registries found in the array we can also overwrite these scopes when Verdaccio is running.
P.S: I am willing to give it a try for my first contribution. Just making sure this is actually a direction Nx wants to go.
GitHub Repo
No response
Steps to Reproduce
Nx Report
Failure Logs
No response
Package Manager Version
npm 10.5.0
Operating System
Additional Information
No response