diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..2bd5a0a98 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/package.json b/package.json index 93539b7bd..6d5463b3c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-react": "^7.37.1", "flow-bin": "^0.314.0", "flow-typed": "^4.1.1", + "glob": "^10.5.0", "hermes-eslint": "^0.36.1", "husky": "^8.0.0", "jest": "^30.2.0", diff --git a/tools/npm/release.js b/tools/npm/release.js index fd1b207fc..14676090d 100755 --- a/tools/npm/release.js +++ b/tools/npm/release.js @@ -12,7 +12,7 @@ const fs = require('fs'); const glob = require('glob'); const path = require('path'); const yargs = require('yargs/yargs'); -const { execSync } = require('child_process'); +const { execSync, execFileSync } = require('child_process'); const { hideBin } = require('yargs/helpers'); const args = yargs(hideBin(process.argv)) @@ -107,39 +107,86 @@ console.log('Package manifest update complete'); // Change working directory to the repo root process.chdir(path.join(__dirname, '../..')); -execSync('npm install', { stdio: 'inherit' }); +// This repo uses yarn (see `packageManager` and CI). Running `npm install` +// here would generate a stray `package-lock.json`, so install with yarn. +execSync('yarn install', { stdio: 'inherit' }); // Commit changes if (commit) { - // add changes - execSync('git add .', { stdio: 'inherit' }); + // Stage only the files this release is expected to touch: the workspace + // manifests plus the yarn lockfile. Using an explicit pathspec instead of + // `git add .` avoids committing unrelated or stray artifacts (e.g. a + // `package-lock.json` if npm was ever run, or build output). + const manifestPaths = workspaces.map(({ packageJsonPath }) => + path.relative(repoRoot, packageJsonPath), + ); + // Pass args as an array (execFileSync, no shell) so paths can't be + // interpreted by the shell. + execFileSync('git', ['add', ...manifestPaths, 'yarn.lock'], { + stdio: 'inherit', + }); // commit - execSync(`git commit -m "${pkgVersion}" --no-verify`, { stdio: 'inherit' }); + execFileSync('git', ['commit', '-m', pkgVersion, '--no-verify'], { + stdio: 'inherit', + }); // tag - // execSync(`git tag -fam ${pkgVersion} "${pkgVersion}"`, { stdio: 'inherit' }); + // execFileSync('git', ['tag', '-f', '-a', '-m', pkgVersion, pkgVersion], { stdio: 'inherit' }); } if (publish) { - const publishCmd = otp == null ? 'npm publish' : `npm publish --otp ${otp}`; + // Scoped packages (@stylexjs/*) default to restricted access on first + // publish, which fails with E402 unless you have a paid plan. All stylex + // packages are public, so always publish with `--access public`. This is a + // no-op for packages that are already public. + const publishArgs = ['publish', '--access', 'public']; + if (otp != null) { + publishArgs.push('--otp', otp); + } // publish public packages + const failed = []; workspaces.forEach(({ directory, packageJson }) => { if (!packageJson.private) { const version = packageJson.version; const packageName = packageJson.name; try { // Check if the version has already been published - execSync(`npm view --silent ${packageName}@${version} version`); + execFileSync( + 'npm', + ['view', '--silent', `${packageName}@${version}`, 'version'], + { stdio: 'ignore' }, + ); console.log( `Skipping ${packageName} as version ${version} has already been published`, ); } catch (error) { - // If the version has not been published, proceed with publishing - execSync(`cd ${directory} && ${publishCmd}`, { - stdio: 'inherit', - }); + // If the version has not been published, proceed with publishing. + // Don't let a single failure (e.g. a package the current user lacks + // publish rights for) abort the whole release — log it and continue + // so the remaining packages still get published. Run in the package + // directory via `cwd` (no shell) instead of `cd