Files
orca/config/scripts/build-workspace-packages.mjs
T
NeilandLance Woodson 4514634faa fix(scripts): resolve pnpm when npx or npm run exports npm's own CLI (#34271)
`npx` and `npm run` set npm_execpath to npm's own npm-cli.js/npx-cli.js,
so resolvePnpmCliInvocation ran npm with pnpm's arguments. Skip npm's CLI
by file name and fall back to PNPM_HOME / pnpm on PATH. Route
build-workspace-packages and terminal layout parity through the shared
resolver instead of their own npm_execpath parsing.

Fixes #29438

Co-authored-by: Lance Woodson <65194841+woodsonl@users.noreply.github.com>
2026-10-26 22:18:56 -07:00

121 lines
3.8 KiB
JavaScript

import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
mkdirSync,
readdirSync,
readFileSync,
realpathSync,
renameSync,
statSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, relative, resolve } from 'node:path'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
const root = resolve(import.meta.dirname, '../..')
const PACKAGES = [{ name: '@orca/process-host', dir: 'src/packages/process-host' }]
// Outputs and build scratch space, not inputs.
const NON_INPUTS = new Set(['dist', 'node_modules', '.dist-build.lock'])
const COMPILER_PACKAGES = ['typescript', 'tsx', '@types/node']
function listFiles(directory, base = directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
if (directory === base && (NON_INPUTS.has(entry.name) || entry.name.startsWith('.dist-'))) {
return []
}
const path = join(directory, entry.name)
return entry.isDirectory()
? listFiles(path, base)
: [relative(base, path).replaceAll('\\', '/')]
})
}
/** Sources, manifest, build script, and the exact installed compiler and Node types. */
export function hashPackageInputs(packageDir) {
const hash = createHash('sha256')
for (const file of listFiles(packageDir).sort()) {
hash
.update(`${file}\0`)
.update(readFileSync(join(packageDir, file)))
.update('\0')
}
const require = createRequire(join(packageDir, 'package.json'))
for (const name of COMPILER_PACKAGES) {
const manifest = realpathSync(require.resolve(`${name}/package.json`))
hash.update(`${manifest}\0`).update(readFileSync(manifest)).update('\0')
}
return hash.digest('hex')
}
/** Size and mtime of every output, so a deleted or edited dist file forces a rebuild. */
function describeOutputs(packageDir) {
const dist = join(packageDir, 'dist')
try {
return listFiles(dist)
.sort()
.map((file) => {
const { size, mtimeMs } = statSync(join(dist, file))
return `${file}:${size}:${mtimeMs}`
})
} catch {
return []
}
}
function stampPath(packageDir) {
return join(packageDir, 'node_modules', '.cache', 'orca-dist-stamp.json')
}
export function isPackageBuildCurrent(packageDir, inputs) {
try {
const stamp = JSON.parse(readFileSync(stampPath(packageDir), 'utf8'))
const outputs = describeOutputs(packageDir)
return (
stamp.inputs === inputs &&
outputs.length > 0 &&
JSON.stringify(stamp.outputs) === JSON.stringify(outputs)
)
} catch {
return false
}
}
function writeStamp(packageDir, inputs) {
const path = stampPath(packageDir)
mkdirSync(dirname(path), { recursive: true })
const temporary = `${path}.${process.pid}`
writeFileSync(temporary, JSON.stringify({ inputs, outputs: describeOutputs(packageDir) }))
renameSync(temporary, path)
}
function runPackageBuild(name) {
// Why not @orca/process-host: this builds it, so its dist may not exist yet.
const { command, prefixArgs, shell } = resolvePnpmCliInvocation()
const result = spawnSync(command, [...prefixArgs, '--filter', name, 'build'], {
cwd: root,
stdio: 'inherit',
shell
})
if (result.error) {
throw result.error
}
return result.status ?? 1
}
if (process.argv[1] && realpathSync(process.argv[1]) === import.meta.filename) {
for (const { name, dir } of PACKAGES) {
const packageDir = join(root, dir)
// Hashed before building: an edit during the build leaves a stale stamp, never a skipped build.
const inputs = hashPackageInputs(packageDir)
if (isPackageBuildCurrent(packageDir, inputs)) {
continue
}
const status = runPackageBuild(name)
if (status !== 0) {
process.exit(status)
}
writeStamp(packageDir, inputs)
}
}