mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* fix(deps): root and mobile resolve one zod, and CI fails if they drift
Mobile type-checks src/shared zod schemas. With mobile on zod 4.6.5 and the
root on 4.6.7, TypeScript treated them as unrelated packages, so wrapped
schemas inferred {} (tests/e2e/helpers/phone-feed-list.ts:154) whenever a
job installed mobile deps.
* fix(deps): guard every package mobile's tsconfig remaps, not only zod
* test(ci): pin the shared-dependency version check to the static phase
181 lines
6.3 KiB
JavaScript
181 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Static, offline consistency gate for src/shared/node-runtime-pin.ts; update-node-runtime-pin.mjs owns the network.
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { join, resolve } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { nodeDistArchiveName, windowsImportLibFile } from './node-dist-archive-name.mjs'
|
|
import { lockedVersion, lockfileRootImporter } from './pnpm-lockfile-root-importer.mjs'
|
|
|
|
// Why require: an ESM import of a .ts file under a typeless package.json prints MODULE_TYPELESS_PACKAGE_JSON.
|
|
const {
|
|
COMPAT_SERVER_TARGETS,
|
|
NODE_RUNTIME_ASSETS,
|
|
NODE_RUNTIME_COMPAT_ASSETS,
|
|
NODE_RUNTIME_PIN,
|
|
SERVER_TARGETS
|
|
} = createRequire(import.meta.url)('../../src/shared/node-runtime-pin.ts')
|
|
|
|
const SHA256 = /^[0-9a-f]{64}$/
|
|
const ASSET_SOURCES = new Set(['official', 'unofficial'])
|
|
|
|
function majorOf(range) {
|
|
const match = /(\d+)/.exec(String(range ?? ''))
|
|
return match ? Number(match[1]) : null
|
|
}
|
|
|
|
function findAssetTableProblems({ pin, assets, targets, table, targetList }) {
|
|
const problems = []
|
|
const expected = new Set(targets)
|
|
for (const target of targets) {
|
|
if (!Object.hasOwn(assets, target)) {
|
|
problems.push(`${table} has no entry for ${target}`)
|
|
}
|
|
}
|
|
for (const [target, asset] of Object.entries(assets)) {
|
|
if (!expected.has(target)) {
|
|
problems.push(`${table} has ${target}, which is not in ${targetList}`)
|
|
continue
|
|
}
|
|
if (!ASSET_SOURCES.has(asset.source)) {
|
|
problems.push(`${target}: source must be official or unofficial, got ${asset.source}`)
|
|
}
|
|
const expectedArchive = nodeDistArchiveName(pin.version, target)
|
|
if (asset.archive !== expectedArchive) {
|
|
problems.push(`${target}: archive ${asset.archive} is not ${expectedArchive}`)
|
|
}
|
|
if (!SHA256.test(asset.archiveSha256 ?? '')) {
|
|
problems.push(`${target}: archiveSha256 is not a 64-character hex SHA-256`)
|
|
}
|
|
if (!SHA256.test(asset.executableSha256 ?? '')) {
|
|
problems.push(`${target}: executableSha256 is not a 64-character hex SHA-256`)
|
|
}
|
|
if (!Number.isInteger(asset.executableSize) || asset.executableSize <= 0) {
|
|
problems.push(`${target}: executableSize must be a positive integer`)
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
export function findNodeRuntimePinProblems({
|
|
pin,
|
|
assets,
|
|
targets,
|
|
compatAssets = {},
|
|
compatTargets = [],
|
|
packageJson,
|
|
rootImporter
|
|
}) {
|
|
const problems = []
|
|
const declaredElectron =
|
|
packageJson.devDependencies?.electron ?? packageJson.dependencies?.electron
|
|
if (declaredElectron !== pin.electron) {
|
|
problems.push(
|
|
`package.json electron is ${declaredElectron}, but NODE_RUNTIME_PIN.electron is ${pin.electron}`
|
|
)
|
|
}
|
|
const lockedElectron = lockedVersion(
|
|
rootImporter.devDependencies?.electron ?? rootImporter.dependencies?.electron
|
|
)
|
|
if (lockedElectron !== pin.electron) {
|
|
problems.push(
|
|
`pnpm-lock.yaml resolves electron ${lockedElectron}, but NODE_RUNTIME_PIN.electron is ${pin.electron}`
|
|
)
|
|
}
|
|
// Only the major is gated here; whether the pin may differ from Electron's Node is design D1
|
|
// (docs/reference/node-runtime-design.html).
|
|
const engineMajor = majorOf(packageJson.engines?.node)
|
|
if (majorOf(pin.version) !== engineMajor) {
|
|
problems.push(
|
|
`NODE_RUNTIME_PIN.version ${pin.version} is not package.json engines.node major ${engineMajor}`
|
|
)
|
|
}
|
|
const runtimeMajor = majorOf(pin.version)
|
|
const declaredNodeTypes =
|
|
packageJson.devDependencies?.['@types/node'] ?? packageJson.dependencies?.['@types/node']
|
|
if (majorOf(declaredNodeTypes) !== runtimeMajor) {
|
|
problems.push(
|
|
`package.json @types/node is ${declaredNodeTypes}, but NODE_RUNTIME_PIN.version major is ${runtimeMajor}`
|
|
)
|
|
}
|
|
const lockedNodeTypes = lockedVersion(
|
|
rootImporter.devDependencies?.['@types/node'] ?? rootImporter.dependencies?.['@types/node']
|
|
)
|
|
if (majorOf(lockedNodeTypes) !== runtimeMajor) {
|
|
problems.push(
|
|
`pnpm-lock.yaml resolves @types/node ${lockedNodeTypes}, but NODE_RUNTIME_PIN.version major is ${runtimeMajor}`
|
|
)
|
|
}
|
|
if (!Number.isInteger(pin.napi) || pin.napi < 1) {
|
|
problems.push(`NODE_RUNTIME_PIN.napi must be a positive integer, got ${pin.napi}`)
|
|
}
|
|
if (!SHA256.test(pin.headers?.sha256 ?? '')) {
|
|
problems.push('NODE_RUNTIME_PIN.headers.sha256 is not a 64-character hex SHA-256')
|
|
}
|
|
if (pin.headers?.file !== `node-v${pin.version}-headers.tar.gz`) {
|
|
problems.push(`NODE_RUNTIME_PIN.headers.file ${pin.headers?.file} is not for ${pin.version}`)
|
|
}
|
|
for (const target of targets.filter((name) => name.startsWith('win32-'))) {
|
|
const lib = pin.windowsImportLibs?.[target]
|
|
if (lib?.file !== windowsImportLibFile(target)) {
|
|
problems.push(
|
|
`NODE_RUNTIME_PIN.windowsImportLibs.${target}.file is not ${windowsImportLibFile(target)}`
|
|
)
|
|
}
|
|
if (!SHA256.test(lib?.sha256 ?? '')) {
|
|
problems.push(
|
|
`NODE_RUNTIME_PIN.windowsImportLibs.${target}.sha256 is not a 64-character hex SHA-256`
|
|
)
|
|
}
|
|
}
|
|
|
|
problems.push(
|
|
...findAssetTableProblems({
|
|
pin,
|
|
assets,
|
|
targets,
|
|
table: 'NODE_RUNTIME_ASSETS',
|
|
targetList: 'SERVER_TARGETS'
|
|
}),
|
|
...findAssetTableProblems({
|
|
pin,
|
|
assets: compatAssets,
|
|
targets: compatTargets,
|
|
table: 'NODE_RUNTIME_COMPAT_ASSETS',
|
|
targetList: 'COMPAT_SERVER_TARGETS'
|
|
})
|
|
)
|
|
return problems
|
|
}
|
|
|
|
export function main(root = resolve(import.meta.dirname, '../..')) {
|
|
const problems = findNodeRuntimePinProblems({
|
|
pin: NODE_RUNTIME_PIN,
|
|
assets: NODE_RUNTIME_ASSETS,
|
|
targets: SERVER_TARGETS,
|
|
compatAssets: NODE_RUNTIME_COMPAT_ASSETS,
|
|
compatTargets: COMPAT_SERVER_TARGETS,
|
|
packageJson: JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')),
|
|
rootImporter: lockfileRootImporter(readFileSync(join(root, 'pnpm-lock.yaml'), 'utf8'))
|
|
})
|
|
if (problems.length > 0) {
|
|
console.error('Node runtime pin check failed:')
|
|
for (const problem of problems) {
|
|
console.error(`- ${problem}`)
|
|
}
|
|
console.error(
|
|
'Regenerate with: node config/scripts/update-node-runtime-pin.mjs --version <x.y.z>'
|
|
)
|
|
return 1
|
|
}
|
|
console.log(
|
|
`Node runtime pin check passed: Node ${NODE_RUNTIME_PIN.version} for Electron ${NODE_RUNTIME_PIN.electron}.`
|
|
)
|
|
return 0
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
process.exit(main())
|
|
}
|