558 lines
17 KiB
JavaScript
558 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createProjectGraphAsync, workspaceRoot } from '@nx/devkit';
|
|
import { styleText } from 'node:util';
|
|
import { execSync } from 'node:child_process';
|
|
import { rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { URL } from 'node:url';
|
|
import { isRelativeVersionKeyword } from 'nx/src/command-line/release/utils/semver';
|
|
import { ReleaseType, major, parse } from 'semver';
|
|
import yargs from 'yargs';
|
|
|
|
const LARGE_BUFFER = 1024 * 1000000;
|
|
|
|
// DO NOT MODIFY, even for testing. This only gates releases to latest.
|
|
const VALID_AUTHORS_FOR_LATEST = [
|
|
'jaysoo',
|
|
'JamesHenry',
|
|
'FrozenPandaz',
|
|
'vsavkin',
|
|
];
|
|
|
|
(async () => {
|
|
const options = parseArgs();
|
|
// Perform minimal logging by default
|
|
let isVerboseLogging = process.env.NX_VERBOSE_LOGGING === 'true';
|
|
|
|
if (options.clearLocalRegistry) {
|
|
rmSync(join(__dirname, '../dist/local-registry/storage'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
|
|
// Ensure all the native-packages directories are available at the top level of the build directory, enabling consistent packageRoot structure
|
|
execSync(`pnpm nx copy-native-package-directories nx`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
// Expected to run as part of the Github `publish` workflow
|
|
if (!options.local && process.env.GITHUB_ACTIONS) {
|
|
// Delete all .node files that were built during the previous steps
|
|
// Always run before the artifacts step because we still need the .node files for native-packages
|
|
execSync('find ./dist ./packages/nx/dist -name "*.node" -delete', {
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
execSync('pnpm nx run-many --target=artifacts', {
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
}
|
|
|
|
const runNxReleaseVersion = () => {
|
|
let versionCommand = `pnpm nx release version${
|
|
options.version ? ` --specifier ${options.version}` : ''
|
|
}`;
|
|
if (options.dryRun) {
|
|
versionCommand += ' --dry-run';
|
|
}
|
|
if (isVerboseLogging) {
|
|
versionCommand += ' --verbose';
|
|
}
|
|
console.log(`> ${versionCommand}`);
|
|
execSync(versionCommand, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
};
|
|
|
|
const packagesToReset = [
|
|
'packages/angular',
|
|
'packages/angular-rspack',
|
|
'packages/angular-rspack-compiler',
|
|
'packages/create-nx-plugin',
|
|
'packages/create-nx-workspace',
|
|
'packages/devkit',
|
|
'packages/detox',
|
|
'packages/dotnet',
|
|
'packages/cypress',
|
|
'packages/docker',
|
|
'packages/esbuild',
|
|
'packages/eslint',
|
|
'packages/eslint-plugin',
|
|
'packages/expo',
|
|
'packages/express',
|
|
'packages/gradle',
|
|
'packages/jest',
|
|
'packages/js',
|
|
'packages/maven',
|
|
'packages/module-federation',
|
|
'packages/nest',
|
|
'packages/next',
|
|
'packages/node',
|
|
'packages/nuxt',
|
|
'packages/nx',
|
|
'packages/playwright',
|
|
'packages/plugin',
|
|
'packages/react',
|
|
'packages/react-native',
|
|
'packages/remix',
|
|
'packages/rollup',
|
|
'packages/rsbuild',
|
|
'packages/rspack',
|
|
'packages/storybook',
|
|
'packages/vite',
|
|
'packages/vitest',
|
|
'packages/vue',
|
|
'packages/web',
|
|
'packages/webpack',
|
|
'packages/workspace',
|
|
];
|
|
|
|
const packageSnapshots: { [key: string]: string } = {};
|
|
for (const packagePath of packagesToReset) {
|
|
const packageJsonPath = join(workspaceRoot, packagePath, 'package.json');
|
|
const packageJson = readFileSync(packageJsonPath, 'utf-8');
|
|
packageSnapshots[packagePath] = packageJson;
|
|
}
|
|
|
|
const resetPackageJsons = () => {
|
|
for (const packagePath of packagesToReset) {
|
|
const packageJsonPath = join(workspaceRoot, packagePath, 'package.json');
|
|
writeFileSync(packageJsonPath, packageSnapshots[packagePath]);
|
|
}
|
|
};
|
|
|
|
// Reset package.json files even on ctrl+C
|
|
process.on('SIGINT', () => {
|
|
console.log('\nReceived SIGINT, restoring package.json files...');
|
|
resetPackageJsons();
|
|
process.exit(1);
|
|
});
|
|
|
|
// Intended for creating a github release which triggers the publishing workflow
|
|
// This runs locally (not in CI) to create the GitHub release that triggers the actual publish workflow
|
|
if (!options.local && !process.env.GITHUB_ACTIONS) {
|
|
// For this important use-case it makes sense to always have full logs
|
|
isVerboseLogging = true;
|
|
|
|
execSync('git status --ahead-behind', {
|
|
windowsHide: false,
|
|
});
|
|
|
|
if (isRelativeVersionKeyword(options.version)) {
|
|
resetPackageJsons();
|
|
throw new Error(
|
|
'When creating actual releases, you must use an exact semver version'
|
|
);
|
|
}
|
|
|
|
try {
|
|
runNxReleaseVersion();
|
|
|
|
execSync(`pnpm nx run-many -t add-extra-dependencies --parallel 8`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
execSync(`pnpm nx run nx:expand-deps`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
let changelogCommand = `pnpm nx release changelog ${options.version} --interactive workspace`;
|
|
if (options.from) {
|
|
changelogCommand += ` --from ${options.from}`;
|
|
}
|
|
if (options.gitRemote) {
|
|
changelogCommand += ` --git-remote ${options.gitRemote}`;
|
|
}
|
|
if (options.dryRun) {
|
|
changelogCommand += ' --dry-run';
|
|
}
|
|
if (isVerboseLogging) {
|
|
changelogCommand += ' --verbose';
|
|
}
|
|
console.log(`> ${changelogCommand}`);
|
|
execSync(changelogCommand, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
console.log(
|
|
'Check github: https://github.com/nrwl/nx/actions/workflows/publish.yml'
|
|
);
|
|
} finally {
|
|
resetPackageJsons();
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
try {
|
|
runNxReleaseVersion();
|
|
|
|
execSync(`pnpm nx run-many -t add-extra-dependencies --parallel 8`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
execSync(`pnpm nx run nx:expand-deps`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
const distTag = determineDistTag(options.version);
|
|
|
|
// If publishing locally, force all projects to not be private first
|
|
if (options.local) {
|
|
console.log(
|
|
styleText(
|
|
'dim',
|
|
`\n Publishing locally, so setting all packages with existing nx-release-publish targets to not be private. If you have created a new private package and you want it to be published, you will need to manually configure the "nx-release-publish" target using executor "@nx/js:release-publish"`
|
|
)
|
|
);
|
|
const projectGraph = await createProjectGraphAsync();
|
|
for (const proj of Object.values(projectGraph.nodes)) {
|
|
if (proj.data.targets?.['nx-release-publish']) {
|
|
const packageJsonPath = join(
|
|
workspaceRoot,
|
|
// Mirror the @nx/js:release-publish executor default: when packageRoot
|
|
// is not set explicitly, it falls back to the project root.
|
|
proj.data.targets?.['nx-release-publish']?.options?.packageRoot ??
|
|
proj.data.root,
|
|
'package.json'
|
|
);
|
|
try {
|
|
const packageJson = require(packageJsonPath);
|
|
if (packageJson.private) {
|
|
console.log(
|
|
'- Publishing private package locally:',
|
|
packageJson.name
|
|
);
|
|
writeFileSync(
|
|
packageJsonPath,
|
|
JSON.stringify({ ...packageJson, private: false })
|
|
);
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!options.local && (!distTag || distTag === 'latest')) {
|
|
// We are only expecting non-local latest releases to be performed within publish.yml on GitHub
|
|
const author = process.env.GITHUB_ACTOR ?? '';
|
|
if (!VALID_AUTHORS_FOR_LATEST.includes(author)) {
|
|
throw new Error(
|
|
`The GitHub user "${author}" is not allowed to publish to "latest". Please request one of the following users to carry out the release: ${VALID_AUTHORS_FOR_LATEST.join(
|
|
', '
|
|
)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Clean up tsconfig files before publishing
|
|
console.log('Cleaning up tsconfig files...');
|
|
execSync('node ./scripts/cleanup-tsconfig-files.js', {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
hackFixForDevkitPeerDependencies();
|
|
|
|
// Run with dynamic output-style so that we have more minimal logs by default but still always see errors
|
|
let publishCommand = `pnpm nx release publish --registry=${getRegistry()} --tag=${distTag} --output-style=dynamic --parallel=8`;
|
|
if (options.dryRun) {
|
|
publishCommand += ' --dry-run';
|
|
}
|
|
console.log(`\n> ${publishCommand}`);
|
|
execSync(publishCommand, {
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
if (!options.dryRun) {
|
|
let version;
|
|
if (['minor', 'major', 'patch'].includes(options.version)) {
|
|
version = execSync(`npm view nx@${distTag} version`, {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
} else {
|
|
version = options.version;
|
|
}
|
|
|
|
console.log(styleText('green', ` > Published version: ` + version));
|
|
console.log(
|
|
styleText('dim', ` Use: npx create-nx-workspace@${version}\n`)
|
|
);
|
|
}
|
|
} finally {
|
|
resetPackageJsons();
|
|
}
|
|
|
|
process.exit(0);
|
|
})();
|
|
|
|
function parseArgs() {
|
|
const registry = getRegistry();
|
|
const registryIsLocalhost = registry.hostname === 'localhost';
|
|
|
|
const parsedArgs = yargs
|
|
.scriptName('pnpm nx-release')
|
|
.wrap(144)
|
|
.strictOptions()
|
|
.version(false)
|
|
.command(
|
|
'$0 [version]',
|
|
'This script is for publishing Nx both locally and publically'
|
|
)
|
|
.option('dryRun', {
|
|
type: 'boolean',
|
|
description: 'Dry-run flag to be passed to all `nx release` commands',
|
|
})
|
|
.option('clearLocalRegistry', {
|
|
type: 'boolean',
|
|
description:
|
|
'Clear existing versions in the local registry so that you can republish the same version',
|
|
default: true,
|
|
})
|
|
.option('local', {
|
|
type: 'boolean',
|
|
description: 'Publish Nx locally, not to actual NPM',
|
|
alias: 'l',
|
|
default: true,
|
|
})
|
|
.option('force', {
|
|
type: 'boolean',
|
|
description: "Don't use this unless you really know what it does",
|
|
hidden: true,
|
|
})
|
|
.option('from', {
|
|
type: 'string',
|
|
description:
|
|
'Git ref to pass to `nx release changelog`. Not applicable for local publishing or e2e tests.',
|
|
})
|
|
.positional('version', {
|
|
type: 'string',
|
|
description:
|
|
'The version to publish. This does not need to be passed and can be inferred.',
|
|
default: 'minor',
|
|
coerce: (version: string) => {
|
|
const isGithubActions = !!process.env.GITHUB_ACTIONS;
|
|
if (
|
|
isGithubActions &&
|
|
!registryIsLocalhost &&
|
|
isRelativeVersionKeyword(version)
|
|
) {
|
|
// Print error rather than throw to avoid yargs noise in this specifically handled case
|
|
console.error(
|
|
'Error: The release script was triggered in a GitHub Actions workflow, to a non-local registry, but a relative version keyword was provided. This is an unexpected combination.'
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (version !== 'canary') {
|
|
return version;
|
|
}
|
|
/**
|
|
* Handle the special case of `canary`
|
|
*
|
|
* Use the base version from nx@next so that canary versions
|
|
* are always aligned with the current next/beta release line.
|
|
*/
|
|
|
|
const currentNextVersion = execSync('npm view nx@next version', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
|
|
const parsedNext = parse(currentNextVersion);
|
|
if (!parsedNext) {
|
|
throw new Error(
|
|
`Unable to parse the current next version from the npm registry: "${currentNextVersion}"`
|
|
);
|
|
}
|
|
|
|
const canaryBaseVersion = `${parsedNext.major}.${parsedNext.minor}.${parsedNext.patch}`;
|
|
|
|
// Create YYYYMMDD string
|
|
const date = new Date();
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const YYYYMMDD = `${year}${month}${day}`;
|
|
|
|
// Get the current short git sha
|
|
const gitSha = execSync('git rev-parse --short HEAD', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
|
|
const canaryVersion = `${canaryBaseVersion}-canary.${YYYYMMDD}-${gitSha}`;
|
|
|
|
console.log(`\nDerived canary version dynamically`, {
|
|
currentNextVersion,
|
|
canaryVersion,
|
|
});
|
|
|
|
return canaryVersion;
|
|
},
|
|
})
|
|
.option('gitRemote', {
|
|
type: 'string',
|
|
description:
|
|
'Alternate git remote name to publish tags to (useful for testing changelog)',
|
|
default: 'origin',
|
|
})
|
|
.example(
|
|
'$0',
|
|
`By default, this will locally publish a minor version bump as latest. Great for local development. Most developers should only need this.`
|
|
)
|
|
.example(
|
|
'$0 --local false 2.3.4-beta.0',
|
|
`This will really publish a new version to npm as next.`
|
|
)
|
|
.example(
|
|
'$0 --local false 2.3.4',
|
|
`Given the current latest major version on npm is 2, this will really publish a new version to npm as latest.`
|
|
)
|
|
.example(
|
|
'$0 --local false 1.3.4-beta.0',
|
|
`Given the current latest major version on npm is 2, this will really publish a new version to npm as previous.`
|
|
)
|
|
.group(
|
|
['local', 'clearLocalRegistry'],
|
|
'Local Publishing Options for most developers'
|
|
)
|
|
.group(
|
|
['gitRemote', 'force'],
|
|
'Real Publishing Options for actually publishing to NPM'
|
|
)
|
|
.demandOption('version')
|
|
.check((args) => {
|
|
if (!args.local) {
|
|
if (!process.env.GH_TOKEN) {
|
|
throw new Error('process.env.GH_TOKEN is not set');
|
|
}
|
|
if (!args.force && registryIsLocalhost) {
|
|
throw new Error(
|
|
'Registry is still set to localhost! Run "pnpm local-registry disable" or pass --force'
|
|
);
|
|
}
|
|
} else {
|
|
if (!args.force && !registryIsLocalhost) {
|
|
throw new Error('--local was passed and registry is not localhost');
|
|
}
|
|
}
|
|
|
|
return true;
|
|
})
|
|
.parseSync();
|
|
|
|
return parsedArgs;
|
|
}
|
|
|
|
function getRegistry() {
|
|
return new URL(
|
|
execSync('npm config get registry', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
function determineDistTag(
|
|
newVersion: string
|
|
): 'latest' | 'next' | 'previous' | 'canary' | 'pull-request' {
|
|
// Special case of canary
|
|
if (newVersion.includes('canary')) {
|
|
return 'canary';
|
|
}
|
|
|
|
// Special case of PR release (e.g. 22.5.0-pr.1234.abc1234)
|
|
if (/^\d+\.\d+\.\d+-pr\./.test(newVersion)) {
|
|
return 'pull-request';
|
|
}
|
|
|
|
// For a relative version keyword, it cannot be previous
|
|
if (isRelativeVersionKeyword(newVersion)) {
|
|
const prereleaseKeywords: ReleaseType[] = [
|
|
'premajor',
|
|
'preminor',
|
|
'prepatch',
|
|
'prerelease',
|
|
];
|
|
return prereleaseKeywords.includes(newVersion) ? 'next' : 'latest';
|
|
}
|
|
|
|
const parsedGivenVersion = parse(newVersion);
|
|
if (!parsedGivenVersion) {
|
|
throw new Error(
|
|
`Unable to parse the given version: "${newVersion}". Is it valid semver?`
|
|
);
|
|
}
|
|
|
|
const currentLatestVersion = execSync('npm view nx version', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
const parsedCurrentLatestVersion = parse(currentLatestVersion);
|
|
if (!parsedCurrentLatestVersion) {
|
|
throw new Error(
|
|
`The current version resolved from the npm registry could not be parsed (resolved "${currentLatestVersion}")`
|
|
);
|
|
}
|
|
|
|
const distTag =
|
|
parsedGivenVersion.prerelease.length > 0
|
|
? 'next'
|
|
: parsedGivenVersion.major < parsedCurrentLatestVersion.major
|
|
? 'previous'
|
|
: 'latest';
|
|
|
|
return distTag;
|
|
}
|
|
|
|
//TODO(@Coly010): Remove this after fixing up the release peer dep handling
|
|
function hackFixForDevkitPeerDependencies() {
|
|
const { readFileSync, writeFileSync } = require('fs');
|
|
const devkitPackageJson = JSON.parse(
|
|
readFileSync('./packages/devkit/package.json', 'utf-8')
|
|
);
|
|
|
|
const beforeVersion = devkitPackageJson.peerDependencies['nx'];
|
|
const majorVersion = major(beforeVersion);
|
|
if (!beforeVersion.includes('<') && majorVersion !== 0) {
|
|
console.log(
|
|
'@nx/devkit peer dependencies range is broken - needs release fix. Patching it to avoid broken publishes.'
|
|
);
|
|
devkitPackageJson.peerDependencies['nx'] = `>= ${majorVersion - 1} <= ${
|
|
majorVersion + 1
|
|
} || ^${majorVersion}.0.0-0`;
|
|
writeFileSync(
|
|
'./packages/devkit/package.json',
|
|
JSON.stringify(devkitPackageJson, null, 2)
|
|
);
|
|
}
|
|
}
|