UNPKG

@nx/js

Version:

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

472 lines (468 loc) 23.4 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 fs_1 = require("fs"); 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; } function isAlreadyPublishedPublishError(stdoutData, stderr = '', stdout = '') { const error = stdoutData?.error; const errorMessage = [ error?.summary, error?.detail, error?.message, error?.body?.error, stderr, stdout, ] .filter(Boolean) .join('\n'); return (error?.code === 'EPUBLISHCONFLICT' || errorMessage.includes('You cannot publish over the previously published versions') || (error?.code === 'E409' && errorMessage.includes('this package is already present'))); } async function runExecutor(options, context) { const pm = (0, devkit_1.detectPackageManager)(); // Check if npm is installed (needed for dist-tag management and as fallback for view command) let isNpmInstalled = false; try { isNpmInstalled = (0, child_process_1.execSync)('npm --version', { encoding: 'utf-8', windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], }).trim() !== ''; } catch { // Allow missing npm only when using bun if (pm !== 'bun') { console.error(`npm was not found in the current environment. This is only supported when using \`bun\` as a package manager, but your detected package manager is "${pm}"`); return { success: false, }; } } /** * 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, }; } /** * If version data was provided by the nx release version step, check if this project * actually had a new version resolved. If not (newVersion is null), there is nothing * to publish, so we can skip this project entirely. */ if (options.nxReleaseVersionData) { const projectVersionData = options.nxReleaseVersionData[context.projectName]; if (projectVersionData && projectVersionData.newVersion === null) { console.warn(`Skipped ${packageTxt}, because no new version was resolved for this project`); 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); // Use bun info when bun is the package manager, otherwise use npm view // (npm view works across npm/pnpm/yarn environments and is the established default) const npmViewCommandSegments = pm === 'bun' ? ['bun info', packageName, `--json --"${registryConfigKey}=${registry}"`] : [ `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: true, }); 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 (isNpmInstalled) { // 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: true, }); 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:'); // npm returns error.summary and error.detail if (stdoutData.error?.summary) { console.error(stdoutData.error.summary); } if (stdoutData.error?.detail) { console.error(stdoutData.error.detail); } // pnpm returns error.code and error.message if (stdoutData.error?.code && !stdoutData.error?.summary) { console.error(`Error code: ${stdoutData.error.code}`); } if (stdoutData.error?.message && !stdoutData.error?.summary) { console.error(stdoutData.error.message); } 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) { 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?.toLowerCase().includes('not found')) && !(err.stderr?.toString().includes('E404') && err.stderr?.toString().toLowerCase().includes('not found')) && // bun uses plain '404' instead of 'E404' !(err.stderr?.toString().includes('404') && err.stderr?.toString().toLowerCase().includes('not found'))) { console.error(`Something unexpected went wrong when checking for existing dist-tags.\n`, err); return { success: false, }; } } catch { // JSON parse failed entirely — check stderr/stdout for plain 404 const stderrStr = err.stderr?.toString() || ''; const stdoutStr = err.stdout?.toString() || ''; if (!((stderrStr.includes('404') && stderrStr.toLowerCase().includes('not found')) || (stdoutStr.includes('404') && stdoutStr.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). */ return runPublish({ pm, options, context, packageRoot, packageJson, registry, registryConfigKey, tag, isDryRun, isNpmInstalled, packageTxt, }); } function runPublish(ctx) { const { pm, options, context, packageRoot, packageJson, registry, registryConfigKey, tag, isDryRun, isNpmInstalled, packageTxt, } = ctx; const pmCommand = (0, devkit_1.getPackageManagerCommand)(pm); const publishCommandSegments = [ pmCommand.publish(packageRoot, registry, registryConfigKey, 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: true, }); // 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, }; } // pnpm 11 dropped the per-file size field from its publish --json output, // so recover the sizes from disk for the tarball contents log. if (Array.isArray(jsonData.files)) { for (const file of jsonData.files) { if (typeof file.size !== 'number' && typeof file.path === 'string') { try { file.size = (0, fs_1.statSync)((0, path_1.join)(packageRoot, file.path)).size; } catch { // leave the size unknown, logTar omits it } } } } 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') { const bunStderr = err.stderr?.toString() || ''; const bunStdout = err.stdout?.toString() || ''; // bun publish does not yet support npm's OIDC trusted publishing flow. If the failure // looks like an authentication error and npm is available, retry via npm to recover. // Other failures (e.g. version conflict, 403) would produce the same error from npm, // so we surface bun's error directly instead. const looksLikeAuthError = /missing authentication|bunx npm login|unauthorized|\b401\b/i.test(bunStderr + bunStdout); if (isNpmInstalled && looksLikeAuthError) { console.warn(`bun publish failed with an authentication error; falling back to npm publish (bun does not support npm OIDC trusted publishing).`); return runPublish({ ...ctx, pm: 'npm' }); } console.error(`bun publish error:`); console.error(bunStderr); console.error(bunStdout); return { success: false, }; } // stdout is not guaranteed to be JSON (e.g. lifecycle script failures). // If parsing fails, print raw stderr/stdout so the underlying error is visible to the user. let stdoutData; try { stdoutData = JSON.parse(err.stdout?.toString() || '{}'); } catch { const stderr = err.stderr?.toString() || ''; const stdout = err.stdout?.toString() || ''; if (isAlreadyPublishedPublishError(undefined, stderr, stdout)) { console.warn(`Skipped ${packageTxt}, as v${packageJson.version} has already been published to ${registry} with tag "${tag}"`); return { success: true, }; } console.error(err.stderr?.toString() || ''); console.error(err.stdout?.toString() || ''); return { success: false, }; } if (isAlreadyPublishedPublishError(stdoutData, err.stderr?.toString() || '', err.stdout?.toString() || '')) { console.warn(`Skipped ${packageTxt}, as v${packageJson.version} has already been published to ${registry} with tag "${tag}"`); return { success: true, }; } console.error(`${pm} publish error:`); // npm returns error.summary and error.detail if (stdoutData.error?.summary) { console.error(stdoutData.error.summary); } if (stdoutData.error?.detail) { console.error(stdoutData.error.detail); } // pnpm returns error.code and error.message if (stdoutData.error?.code && !stdoutData.error?.summary) { console.error(`Error code: ${stdoutData.error.code}`); } if (stdoutData.error?.message && !stdoutData.error?.summary) { console.error(stdoutData.error.message); } 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, }; } } }