UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

318 lines (314 loc) • 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = runExecutor; const devkit_1 = require("@nx/devkit"); const child_process_1 = require("child_process"); const npm_run_path_1 = require("npm-run-path"); const path_1 = require("path"); const is_locally_linked_package_version_1 = require("../../utils/is-locally-linked-package-version"); const npm_config_1 = require("../../utils/npm-config"); const extract_npm_publish_json_data_1 = require("./extract-npm-publish-json-data"); const log_tar_1 = require("./log-tar"); const chalk = require("chalk"); const LARGE_BUFFER = 1024 * 1000000; function processEnv(color) { const env = { ...process.env, ...(0, npm_run_path_1.env)(), }; if (color) { env.FORCE_COLOR = `${color}`; } return env; } async function runExecutor(options, context) { const pm = (0, devkit_1.detectPackageManager)(); /** * We need to check both the env var and the option because the executor may have been triggered * indirectly via dependsOn, in which case the env var will be set, but the option will not. */ const isDryRun = process.env.NX_DRY_RUN === 'true' || options.dryRun || false; const projectConfig = context.projectsConfigurations.projects[context.projectName]; const packageRoot = (0, path_1.join)(context.root, options.packageRoot ?? projectConfig.root); const packageJsonPath = (0, path_1.join)(packageRoot, 'package.json'); const packageJson = (0, devkit_1.readJsonFile)(packageJsonPath); const packageName = packageJson.name; /** * Whether or not dynamically replacing local dependency protocols (such as "workspace:*") is supported during `nx release publish` is * dependent on the package manager the user is using. * * npm does not support the workspace protocol at all, and `npm publish` does not support dynamically updating locally linked packages * during its packing phase, so we give the user a clear error message informing them of that. * * - `pnpm publish` provides ideal support, it has the possibility of providing JSON output consistent with npm * - `bun publish`, provides very good support, including all the flags we need apart from the JSON output, so we just have to accept that * it will look and feel different and print what it gives us and perform one bit of string manipulation for the dry-run case. * - `yarn npm publish`, IS NOT YET SUPPORTED, and will be tricky because it does not support the majority of the flags we need. However, it * does support replacing local dependency protocols with the correct version during its packing phase. */ if (pm === 'npm' || pm === 'yarn') { const depTypes = ['dependencies', 'devDependencies', 'peerDependencies']; for (const depType of depTypes) { const deps = packageJson[depType]; if (deps) { for (const depName in deps) { if ((0, is_locally_linked_package_version_1.isLocallyLinkedPackageVersion)(deps[depName])) { if (pm === 'npm') { console.error(`Error: Cannot publish package "${packageName}" because it contains a local dependency protocol in its "${depType}", and your package manager is npm. Please update the local dependency on "${depName}" to be a valid semantic version (e.g. using \`nx release\`) before publishing, or switch to pnpm or bun as a package manager, which support dynamically replacing these protocols during publishing.`); } else if (pm === 'yarn') { console.error(`Error: Cannot publish package "${packageName}" because it contains a local dependency protocol in its "${depType}", and your package manager is yarn. Currently, yarn is not supported for this use case because its \`yarn npm publish\` command does not support the customization needed. Please update the local dependency on "${depName}" to be a valid semantic version (e.g. using \`nx release\`) before publishing, or switch to pnpm or bun as a package manager, which support dynamically replacing these protocols during publishing.`); } return { success: false, }; } } } } } // If package and project name match, we can make log messages terser let packageTxt = packageName === context.projectName ? `package "${packageName}"` : `package "${packageName}" from project "${context.projectName}"`; if (packageJson.private === true) { console.warn(`Skipped ${packageTxt}, because it has \`"private": true\` in ${packageJsonPath}`); return { success: true, }; } const warnFn = (message) => { console.log(chalk.keyword('orange')(message)); }; const { registry, tag, registryConfigKey } = await (0, npm_config_1.parseRegistryOptions)(context.root, { packageRoot, packageJson, }, { registry: options.registry, tag: options.tag, }, warnFn); const npmViewCommandSegments = [ `npm view ${packageName} versions dist-tags --json --"${registryConfigKey}=${registry}"`, ]; const npmDistTagAddCommandSegments = [ `npm dist-tag add ${packageName}@${packageJson.version} ${tag} --"${registryConfigKey}=${registry}"`, ]; /** * In a dry-run scenario, it is most likely that all commands are being run with dry-run, therefore * the most up to date/relevant version might not exist on disk for us to read and make the npm view * request with. * * Therefore, so as to not produce misleading output in dry around dist-tags being altered, we do not * perform the npm view step, and just show npm/pnpm publish's dry-run output. */ if (!isDryRun && !options.firstRelease) { const currentVersion = packageJson.version; try { const result = (0, child_process_1.execSync)(npmViewCommandSegments.join(' '), { env: processEnv(true), cwd: context.root, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: false, }); const resultJson = JSON.parse(result.toString()); const distTags = resultJson['dist-tags'] || {}; if (distTags[tag] === currentVersion) { console.warn(`Skipped ${packageTxt} because v${currentVersion} already exists in ${registry} with tag "${tag}"`); return { success: true, }; } // If only one version of a package exists in the registry, versions will be a string instead of an array. const versions = Array.isArray(resultJson.versions) ? resultJson.versions : [resultJson.versions]; if (versions.includes(currentVersion)) { try { if (!isDryRun) { (0, child_process_1.execSync)(npmDistTagAddCommandSegments.join(' '), { env: processEnv(true), cwd: context.root, stdio: 'ignore', windowsHide: false, }); console.log(`Added the dist-tag ${tag} to v${currentVersion} for registry ${registry}.\n`); } else { console.log(`Would add the dist-tag ${tag} to v${currentVersion} for registry ${registry}, but ${chalk.keyword('orange')('[dry-run]')} was set.\n`); } return { success: true, }; } catch (err) { try { const stdoutData = JSON.parse(err.stdout?.toString() || '{}'); // If the error is that the package doesn't exist, then we can ignore it because we will be publishing it for the first time in the next step if (!(stdoutData.error?.code?.includes('E404') && stdoutData.error?.summary?.includes('no such package available')) && !(err.stderr?.toString().includes('E404') && err.stderr?.toString().includes('no such package available'))) { console.error('npm dist-tag add error:'); if (stdoutData.error.summary) { console.error(stdoutData.error.summary); } if (stdoutData.error.detail) { console.error(stdoutData.error.detail); } if (context.isVerbose) { console.error('npm dist-tag add stdout:'); console.error(JSON.stringify(stdoutData, null, 2)); } return { success: false, }; } } catch (err) { console.error('Something unexpected went wrong when processing the npm dist-tag add output\n', err); return { success: false, }; } } } } catch (err) { const stdoutData = JSON.parse(err.stdout?.toString() || '{}'); // If the error is that the package doesn't exist, then we can ignore it because we will be publishing it for the first time in the next step if (!(stdoutData.error?.code?.includes('E404') && stdoutData.error?.summary?.toLowerCase().includes('not found')) && !(err.stderr?.toString().includes('E404') && err.stderr?.toString().toLowerCase().includes('not found'))) { console.error(`Something unexpected went wrong when checking for existing dist-tags.\n`, err); return { success: false, }; } } } if (options.firstRelease && context.isVerbose) { console.log('Skipped npm view because --first-release was set'); } /** * NOTE: If this is ever changed away from running the command at the workspace root and pointing at the package root (e.g. back * to running from the package root directly), then special attention should be paid to the fact that npm/pnpm publish will nest its * JSON output under the name of the package in that case (and it would need to be handled below). */ const publishCommandSegments = [ pm === 'bun' ? // Unlike npm, bun publish does not support a custom registryConfigKey option `bun publish --cwd="${packageRoot}" --json --registry="${registry}" --tag=${tag}` : pm === 'pnpm' ? // Unlike npm, pnpm publish does not support a custom registryConfigKey option, and will error on uncommitted changes by default if --no-git-checks is not set `pnpm publish "${packageRoot}" --json --registry="${registry}" --tag=${tag} --no-git-checks` : `npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`, ]; if (options.otp) { publishCommandSegments.push(`--otp=${options.otp}`); } if (options.access) { publishCommandSegments.push(`--access=${options.access}`); } if (isDryRun) { publishCommandSegments.push(`--dry-run`); } try { const output = (0, child_process_1.execSync)(publishCommandSegments.join(' '), { maxBuffer: LARGE_BUFFER, env: processEnv(true), cwd: context.root, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: false, }); // If in dry-run mode, the version on disk will not represent the version that would be published, so we scrub it from the output to avoid confusion. const dryRunVersionPlaceholder = 'X.X.X-dry-run'; const publishSummaryMessage = isDryRun ? `Would publish to ${registry} with tag "${tag}", but ${chalk.keyword('orange')('[dry-run]')} was set` : `Published to ${registry} with tag "${tag}"`; // bun publish does not support outputting JSON, so we need to modify and print the output string directly if (pm === 'bun') { let outputStr = output.toString(); if (isDryRun) { outputStr = outputStr.replace(new RegExp(`${packageJson.name}@${packageJson.version}`, 'g'), `${packageJson.name}@${dryRunVersionPlaceholder}`); } console.log(outputStr); console.log(publishSummaryMessage); return { success: true, }; } /** * We cannot JSON.parse the output directly because if the user is using lifecycle scripts, npm/pnpm will mix its publish output with the JSON output all on stdout. * Additionally, we want to capture and show the lifecycle script outputs as beforeJsonData and afterJsonData and print them accordingly below. */ const { beforeJsonData, jsonData, afterJsonData } = (0, extract_npm_publish_json_data_1.extractNpmPublishJsonData)(output.toString()); if (!jsonData) { console.error(`The ${pm} publish output data could not be extracted. Please report this issue on https://github.com/nrwl/nx`); return { success: false, }; } if (isDryRun) { for (const [key, val] of Object.entries(jsonData)) { if (typeof val !== 'string') { continue; } jsonData[key] = val.replace(new RegExp(packageJson.version, 'g'), dryRunVersionPlaceholder); } } if (typeof beforeJsonData === 'string' && beforeJsonData.trim().length > 0) { console.log(beforeJsonData); } (0, log_tar_1.logTar)(jsonData); if (typeof afterJsonData === 'string' && afterJsonData.trim().length > 0) { console.log(afterJsonData); } // Print the summary message after the JSON data has been printed console.log(publishSummaryMessage); return { success: true, }; } catch (err) { try { // bun publish does not support outputting JSON, so we cannot perform any further processing if (pm === 'bun') { console.error(`bun publish error:`); console.error(err.stderr?.toString() || ''); console.error(err.stdout?.toString() || ''); return { success: false, }; } const stdoutData = JSON.parse(err.stdout?.toString() || '{}'); console.error(`${pm} publish error:`); if (stdoutData.error?.summary) { console.error(stdoutData.error.summary); } if (stdoutData.error?.detail) { console.error(stdoutData.error.detail); } if (context.isVerbose) { console.error(`${pm} publish stdout:`); console.error(JSON.stringify(stdoutData, null, 2)); } if (!stdoutData.error) { throw err; } return { success: false, }; } catch (err) { console.error(`Something unexpected went wrong when processing the ${pm} publish output\n`, err); return { success: false, }; } } }