fix(ci): preserve pnpm links in packed probes

This commit is contained in:
Tianyi Cui
2026-07-21 18:11:24 +08:00
parent 4e021b4686
commit d4b126862e

View File

@@ -3,15 +3,12 @@
import { spawnSync } from 'node:child_process' import { spawnSync } from 'node:child_process'
import { import {
copyFileSync, copyFileSync,
existsSync,
globSync, globSync,
mkdirSync, mkdirSync,
mkdtempSync, mkdtempSync,
readFileSync, readFileSync,
rmSync, rmSync,
symlinkSync,
} from 'node:fs' } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, resolve } from 'node:path' import { dirname, resolve } from 'node:path'
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
@@ -19,7 +16,6 @@ const root = resolve(import.meta.dirname, '..')
const loaderUrl = pathToFileURL(resolve(root, 'vendor/loader/lib/index.js')).href const loaderUrl = pathToFileURL(resolve(root, 'vendor/loader/lib/index.js')).href
const failures = [] const failures = []
const manifests = globSync('packages/*/*/package.json', { cwd: root }).sort() const manifests = globSync('packages/*/*/package.json', { cwd: root }).sort()
const stagingRoot = mkdtempSync(resolve(tmpdir(), 'dsh-built-package-invariants-'))
const packArgs = ['pack', '--dry-run', '--json', '--ignore-scripts'] const packArgs = ['pack', '--dry-run', '--json', '--ignore-scripts']
// Windows cannot spawn npm's .cmd shim directly; setup-node installs this JS // Windows cannot spawn npm's .cmd shim directly; setup-node installs this JS
// entrypoint beside node.exe, so the probe stays shell-free on every runner. // entrypoint beside node.exe, so the probe stays shell-free on every runner.
@@ -27,38 +23,41 @@ const npmInvocation = process.platform === 'win32'
? [process.execPath, [resolve(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...packArgs]] ? [process.execPath, [resolve(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...packArgs]]
: ['npm', packArgs] : ['npm', packArgs]
try { for (const manifestPath of manifests) {
for (const [index, manifestPath] of manifests.entries()) { const packageDir = dirname(resolve(root, manifestPath))
const packageDir = dirname(resolve(root, manifestPath)) const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8'))
const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8')) const packageName = manifest.name
const packageName = manifest.name if (typeof packageName !== 'string' || packageName.length === 0) {
if (typeof packageName !== 'string' || packageName.length === 0) { failures.push(`${manifestPath}: missing package name`)
failures.push(`${manifestPath}: missing package name`) continue
continue }
}
const pack = spawnSync(npmInvocation[0], npmInvocation[1], { const pack = spawnSync(npmInvocation[0], npmInvocation[1], {
cwd: packageDir, cwd: packageDir,
encoding: 'utf8', encoding: 'utf8',
}) })
if (pack.status !== 0) { if (pack.status !== 0) {
const detail = pack.error?.message const detail = pack.error?.message
?? (pack.stderr.trim() || pack.stdout.trim() || `npm pack exited ${pack.status}`) ?? (pack.stderr.trim() || pack.stdout.trim() || `npm pack exited ${pack.status}`)
failures.push(`${packageName}: ${detail}`) failures.push(`${packageName}: ${detail}`)
continue continue
} }
let files let files
try { try {
const result = JSON.parse(pack.stdout) const result = JSON.parse(pack.stdout)
files = result[0]?.files files = result[0]?.files
if (!Array.isArray(files)) throw new Error('npm pack returned no file inventory') if (!Array.isArray(files)) throw new Error('npm pack returned no file inventory')
} catch (error) { } catch (error) {
failures.push(`${packageName}: cannot parse npm pack inventory: ${String(error)}`) failures.push(`${packageName}: cannot parse npm pack inventory: ${String(error)}`)
continue continue
} }
const stagedPackageDir = resolve(stagingRoot, String(index)) // Keep the packed view below its owning package so Node reaches the real
// pnpm dependency links. Junctioning node_modules elsewhere breaks pnpm's
// relative workspace links on Windows.
const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-packed-invariant-'))
try {
for (const file of files) { for (const file of files) {
if (typeof file.path !== 'string' if (typeof file.path !== 'string'
|| (file.path !== 'package.json' && !file.path.startsWith('lib/'))) continue || (file.path !== 'package.json' && !file.path.startsWith('lib/'))) continue
@@ -66,14 +65,6 @@ try {
mkdirSync(dirname(target), { recursive: true }) mkdirSync(dirname(target), { recursive: true })
copyFileSync(resolve(packageDir, file.path), target) copyFileSync(resolve(packageDir, file.path), target)
} }
const packageNodeModules = resolve(packageDir, 'node_modules')
if (existsSync(packageNodeModules)) {
symlinkSync(
packageNodeModules,
resolve(stagedPackageDir, 'node_modules'),
process.platform === 'win32' ? 'junction' : 'dir',
)
}
const probe = ` const probe = `
const companion = await import(${JSON.stringify(`${packageName}/invariant`)}); const companion = await import(${JSON.stringify(`${packageName}/invariant`)});
@@ -92,13 +83,14 @@ try {
cwd: stagedPackageDir, cwd: stagedPackageDir,
encoding: 'utf8', encoding: 'utf8',
}) })
if (result.status === 0) continue if (result.status !== 0) {
const detail = result.error?.message const detail = result.error?.message
?? (result.stderr.trim() || result.stdout.trim() || `node exited ${result.status}`) ?? (result.stderr.trim() || result.stdout.trim() || `node exited ${result.status}`)
failures.push(`${packageName}: ${detail}`) failures.push(`${packageName}: ${detail}`)
}
} finally {
rmSync(stagedPackageDir, { recursive: true, force: true })
} }
} finally {
rmSync(stagingRoot, { recursive: true, force: true })
} }
if (failures.length > 0) { if (failures.length > 0) {