mirror of
https://github.com/zhu1090093659/dsh-web.git
synced 2026-09-28 14:24:03 +08:00
The skin, pet and community packages left this repository, so packages/skins/ never exists in a checkout of it. Every scanner that still walked it as a second root did so defensively — family-packages.mjs and coverage-gate.discoverPackages listed it first and guarded the walk with existsSync, aggregate.mjs looked for a second aggregate manifest and a second package index under it, e2e-mount-rewrite searched it for workspace packages, and seven test files built fixtures under it. The walkers now name packages/ as the single root and the fixtures and comments follow; modules that only carried the root in a doc comment are reworded. scripts/coverage-baseline.json loses the three packages no longer measured here; its sixteen keys now name exactly the sixteen package directories on disk. The in-repo fallback paths in scripts/market-build stay untouched: they are the documented route for building from a checkout that still carries the packages, and the fixture-based market-build tests populate them deliberately.
360 lines
14 KiB
JavaScript
Executable File
360 lines
14 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
/**
|
||
* Rewrite the packed dsh-web-all tarball's dependency table for the
|
||
* e2e mount smoke (scripts/e2e-mount.sh).
|
||
*
|
||
* Modes:
|
||
* auto (default): every @linxin666/* dependency that already exists on
|
||
* npm at the packed version stays pointed at the registry (the gate's
|
||
* original "npm consumers can install this" assertion is unchanged);
|
||
* only a dependency NOT yet published — a brand-new family package in
|
||
* the push-to-publish window — is packed from its workspace directory
|
||
* and rewritten to a file: tarball. This removes the red window without
|
||
* weakening the gate for anything already published.
|
||
* --family-dir DIR: manual override (the old FAMILY_TGZS_DIR behavior):
|
||
* rewrite every @linxin666/* dependency this repository builds to the
|
||
* same-named tarball in DIR. A family-scoped dependency that is not a
|
||
* workspace package here — an extracted satellite consumed from npm —
|
||
* keeps resolving from the registry, while a workspace package the
|
||
* directory fails to cover stays a loud failure.
|
||
*
|
||
* Tarballs switched to a file: spec are patched recursively: the packed
|
||
* (or copied) tarball's own package.json goes through the same dependency
|
||
* rewrite, so a nested family edge (dsh-skins -> dsh-client-ui-skin-center)
|
||
* cannot fall back to the not-yet-published registry version while npm is
|
||
* still propagating the release. Already-published siblings keep resolving
|
||
* from npm — the registry probe stays authoritative.
|
||
*
|
||
* Usage:
|
||
* node scripts/e2e-mount-rewrite <tarball package.json> [--root DIR]
|
||
* [--family-dir DIR]
|
||
*
|
||
* Exit code is 0 on success; a missing workspace package or tarball exits 1.
|
||
*/
|
||
'use strict'
|
||
|
||
const fs = require('node:fs')
|
||
const os = require('node:os')
|
||
const path = require('node:path')
|
||
const { execFileSync } = require('node:child_process')
|
||
const semver = require('semver')
|
||
|
||
const FAMILY_SCOPE = '@linxin666/'
|
||
const REGISTRY = 'https://registry.npmjs.org'
|
||
|
||
/** GNU tar reads a `C:\...` argument as a remote host spec; --force-local keeps it a local path. */
|
||
const TAR_LOCAL = process.platform === 'win32' ? ['--force-local'] : []
|
||
|
||
/**
|
||
* True when a dependency spec is served by the published version list.
|
||
*
|
||
* The spec is an exact version for family packages declared through the
|
||
* workspace protocol (pnpm writes the resolved version into the packed
|
||
* tarball) and a semver range for plugins consumed straight from npm, so both
|
||
* shapes must be evaluated. A range has to resolve against some published
|
||
* version; treating it as an exact version makes an already-released package
|
||
* look unpublished, and the gate then substitutes a workspace tarball that no
|
||
* longer exists.
|
||
*/
|
||
function resolvesFromPublished(spec, versions) {
|
||
if (typeof spec !== 'string' || spec === '') return false
|
||
if (versions.includes(spec)) return true
|
||
if (!semver.validRange(spec)) return false
|
||
return versions.some(version => semver.satisfies(version, spec))
|
||
}
|
||
|
||
/** Default registry probe: true when the dependency spec resolves from npm. */
|
||
async function checkPublished(name, spec) {
|
||
const res = await fetch(REGISTRY + '/' + name.replace('/', '%2f'))
|
||
if (!res.ok) return false
|
||
const data = await res.json()
|
||
const versions = Array.isArray(data.versions) ? data.versions : Object.keys(data.versions ?? {})
|
||
return resolvesFromPublished(spec, versions)
|
||
}
|
||
|
||
/**
|
||
* Default packer: pnpm pack one workspace directory and return that tarball.
|
||
* Each call uses a unique subdirectory of outDir so a second unpublished
|
||
* family package cannot pick up the previous tarball via readdir + sort().pop().
|
||
*/
|
||
function packWorkspace(pkgDir, outDir) {
|
||
const dest = fs.mkdtempSync(path.join(outDir, 'pack-'))
|
||
// win32: CreateProcess only appends .exe to bare names, so the pnpm.cmd
|
||
// shim must be named explicitly (libuv runs .cmd via cmd.exe).
|
||
const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
||
execFileSync(pnpm, ['pack', '--pack-destination', dest], { cwd: pkgDir, stdio: 'pipe', shell: process.platform === 'win32' })
|
||
const names = fs.readdirSync(dest).filter(name => name.endsWith('.tgz'))
|
||
if (names.length !== 1) {
|
||
throw new Error('pnpm pack 未产出唯一 tarball(' + pkgDir + '): ' + names.join(', '))
|
||
}
|
||
return path.join(dest, names[0])
|
||
}
|
||
|
||
/** Locate a workspace package directory by its package name. */
|
||
function findWorkspacePackage(root, name) {
|
||
const dirs = [path.join(root, 'packages')]
|
||
for (const base of dirs) {
|
||
if (!fs.existsSync(base)) continue
|
||
for (const entry of fs.readdirSync(base)) {
|
||
const manifest = path.join(base, entry, 'package.json')
|
||
if (!fs.existsSync(manifest)) continue
|
||
try {
|
||
if (JSON.parse(fs.readFileSync(manifest, 'utf8')).name === name) return path.join(base, entry)
|
||
} catch { /* ignore unreadable manifests */ }
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/** Read a tarball's embedded package name (family-dir manual mode). */
|
||
function readTgzName(tgz) {
|
||
const raw = execFileSync('tar', [...TAR_LOCAL, '-xzf', tgz, '-O', 'package/package.json'], { stdio: 'pipe' }).toString()
|
||
return JSON.parse(raw).name
|
||
}
|
||
|
||
/** Extract a tarball into a fresh staging directory. */
|
||
function extractTarball(tgz) {
|
||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-tgz-extract-'))
|
||
execFileSync('tar', [...TAR_LOCAL, '-xzf', tgz, '-C', staging], { stdio: 'pipe' })
|
||
return staging
|
||
}
|
||
|
||
/** Repack the staging package/ dir as the given tarball, then drop staging. */
|
||
function repackTarball(tgz, staging) {
|
||
execFileSync('tar', [...TAR_LOCAL, '-czf', tgz, '-C', staging, 'package'], { stdio: 'pipe' })
|
||
fs.rmSync(staging, { recursive: true, force: true })
|
||
}
|
||
|
||
/**
|
||
* Rewrite one package.json's @linxin666/* dependency table. Shared by the
|
||
* aggregate entry manifest and every nested family tarball; `prefix`
|
||
* decorates the report lines so nested rewrites stay distinguishable.
|
||
*/
|
||
async function rewriteManifestDeps(pkgPath, opts, state, prefix) {
|
||
const log = opts.log ?? (() => {})
|
||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||
// A nested family tarball without dependencies has nothing to rewrite;
|
||
// only the aggregate entry enforces a dependency table.
|
||
if (!pkg.dependencies) return []
|
||
const report = []
|
||
const familyNames = Object.keys(pkg.dependencies).filter(key => key.startsWith(FAMILY_SCOPE))
|
||
|
||
if (opts.familyDir) {
|
||
for (const name of familyNames) {
|
||
// The override directory covers the family packages this repository
|
||
// builds. A family-scoped dependency that is not a workspace package is
|
||
// consumed from npm (an extracted satellite), so it keeps resolving from
|
||
// the registry; a workspace package the directory does not cover is
|
||
// still a loud failure.
|
||
if (!familyDirTgz(name, opts, state)) {
|
||
if (findWorkspacePackage(opts.root, name)) {
|
||
throw new Error('缺少本地 tarball:' + name)
|
||
}
|
||
report.push(name + ' 不在本仓 workspace,保持 registry 安装')
|
||
continue
|
||
}
|
||
const tgz = await ensureFamilyDirTgz(name, opts, state)
|
||
pkg.dependencies[name] = 'file:' + tgz
|
||
report.push(name + ' → file:' + tgz + '(family-dir 全覆盖)')
|
||
}
|
||
} else {
|
||
const check = opts.checkPublished ?? checkPublished
|
||
for (const name of familyNames) {
|
||
const version = pkg.dependencies[name]
|
||
if (await check(name, version)) {
|
||
report.push(name + '@' + version + ' npm 已发布(保持 registry 安装)')
|
||
continue
|
||
}
|
||
const tgz = await ensurePackedTgz(name, version, opts, state)
|
||
pkg.dependencies[name] = 'file:' + tgz
|
||
report.push(name + '@' + version + ' npm 未发布 → file:' + tgz + '(发布窗口本地 tarball)')
|
||
}
|
||
}
|
||
|
||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
||
const decorated = prefix === '' ? report : report.map(line => prefix + line)
|
||
for (const line of decorated) log(line)
|
||
return decorated
|
||
}
|
||
|
||
/**
|
||
* Patch one family tarball in place: extract, rewrite its own family
|
||
* dependencies with the same rules, repack. A family tarball switched to
|
||
* file: must not keep an unpublished sibling on the registry — the nested
|
||
* edge is exactly what made the publish-window smoke fail.
|
||
*/
|
||
async function patchTarball(tgz, opts, state) {
|
||
const staging = extractTarball(tgz)
|
||
try {
|
||
const pkgPath = path.join(staging, 'package', 'package.json')
|
||
if (!fs.existsSync(pkgPath)) {
|
||
throw new Error('tarball 缺少 package/package.json:' + tgz)
|
||
}
|
||
await rewriteManifestDeps(pkgPath, opts, state, '[嵌套] ')
|
||
repackTarball(tgz, staging)
|
||
} catch (error) {
|
||
fs.rmSync(staging, { recursive: true, force: true })
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/** Per-run state: patched-tarball cache, recursion guards, scratch dirs. */
|
||
function createState() {
|
||
return {
|
||
patched: new Map(),
|
||
pendingPath: new Map(),
|
||
inProgress: new Set(),
|
||
packedTgz: new Set(),
|
||
familyTgzByName: null,
|
||
packOut: fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-family-tgz-')),
|
||
scratch: fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-family-patch-')),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Auto mode: pack an unpublished family package from the workspace and
|
||
* patch its own family deps; returns the patched tarball path.
|
||
*/
|
||
async function ensurePackedTgz(name, version, opts, state) {
|
||
const key = name + '@' + version
|
||
const cached = state.patched.get(key)
|
||
if (cached) return cached
|
||
if (state.inProgress.has(key)) {
|
||
// Cyclic family edge: the path was assigned on first pack, before
|
||
// patching started; the caller can reference it directly.
|
||
return state.pendingPath.get(key)
|
||
}
|
||
const dir = findWorkspacePackage(opts.root, name)
|
||
if (!dir) throw new Error('npm 未发布且仓库内找不到 workspace 包:' + name)
|
||
// A private workspace package can never be published: the auto-mode
|
||
// publish-window pack must not mask it, or the aggregate ships a
|
||
// registry dependency that fails every consumer install.
|
||
let manifest
|
||
try {
|
||
manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
||
} catch (error) {
|
||
throw new Error('workspace 包清单不可读:' + name + '(' + error.message + ')')
|
||
}
|
||
if (manifest.private === true) {
|
||
throw new Error('npm 未发布且 workspace 包为 private(永远不会发布):' + name + ' —— 公有聚合包不能依赖私有包')
|
||
}
|
||
const pack = opts.pack ?? packWorkspace
|
||
const tgz = pack(dir, state.packOut)
|
||
if (state.packedTgz.has(tgz)) {
|
||
throw new Error('pack 给 ' + name + ' 返回了已占用的 tarball:' + tgz)
|
||
}
|
||
state.packedTgz.add(tgz)
|
||
state.inProgress.add(key)
|
||
state.pendingPath.set(key, tgz)
|
||
try {
|
||
await patchTarball(tgz, opts, state)
|
||
state.patched.set(key, tgz)
|
||
} finally {
|
||
state.inProgress.delete(key)
|
||
state.pendingPath.delete(key)
|
||
}
|
||
return tgz
|
||
}
|
||
|
||
/**
|
||
* Family-dir mode: copy the same-named tarball from the override dir to a
|
||
* scratch location, patch the copy (its own family deps follow the same
|
||
* rules), and return the patched copy path. The override dir stays
|
||
* read-only.
|
||
*/
|
||
function familyDirTgz(name, opts, state) {
|
||
if (!state.familyTgzByName) {
|
||
state.familyTgzByName = new Map()
|
||
for (const file of fs.readdirSync(opts.familyDir)) {
|
||
if (!file.endsWith('.tgz')) continue
|
||
state.familyTgzByName.set(readTgzName(path.join(opts.familyDir, file)), path.join(opts.familyDir, file))
|
||
}
|
||
}
|
||
return state.familyTgzByName.get(name)
|
||
}
|
||
|
||
async function ensureFamilyDirTgz(name, opts, state) {
|
||
const key = 'family:' + name
|
||
const cached = state.patched.get(key)
|
||
if (cached) return cached
|
||
if (state.inProgress.has(key)) return state.pendingPath.get(key)
|
||
const src = familyDirTgz(name, opts, state)
|
||
if (!src) throw new Error('缺少本地 tarball:' + name)
|
||
const dest = fs.mkdtempSync(path.join(state.scratch, 'family-'))
|
||
const copy = path.join(dest, path.basename(src))
|
||
fs.copyFileSync(src, copy)
|
||
state.inProgress.add(key)
|
||
state.pendingPath.set(key, copy)
|
||
try {
|
||
await patchTarball(copy, opts, state)
|
||
state.patched.set(key, copy)
|
||
} finally {
|
||
state.inProgress.delete(key)
|
||
state.pendingPath.delete(key)
|
||
}
|
||
return copy
|
||
}
|
||
|
||
/**
|
||
* Rewrite the aggregate tarball's dependency table in place.
|
||
* @param {object} opts
|
||
* @param {string} opts.pkgPath - path to the extracted tarball's package.json.
|
||
* @param {string} opts.root - repository root (workspace package lookup).
|
||
* @param {string} [opts.familyDir] - manual override directory of family tarballs.
|
||
* @param {(name: string, version: string) => Promise<boolean>} [opts.checkPublished] - registry probe (auto mode).
|
||
* @param {(pkgDir: string, outDir: string) => string} [opts.pack] - workspace packer (auto mode).
|
||
* @param {(message: string) => void} [opts.log]
|
||
* @returns {Promise<string[]>} human-readable report lines.
|
||
*/
|
||
async function rewriteDependencies(opts) {
|
||
const { pkgPath } = opts
|
||
const log = opts.log ?? (() => {})
|
||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||
if (!pkg.dependencies) throw new Error('tarball package.json 没有 dependencies')
|
||
const report = []
|
||
|
||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
||
|
||
const state = createState()
|
||
const nested = await rewriteManifestDeps(pkgPath, { ...opts, log }, state, '')
|
||
return report.concat(nested)
|
||
}
|
||
|
||
function parseArgs(argv) {
|
||
const opts = { root: path.resolve(__dirname, '..') }
|
||
const positional = []
|
||
for (let i = 0; i < argv.length; i += 1) {
|
||
if (argv[i] === '--root') opts.root = argv[++i]
|
||
else if (argv[i] === '--family-dir') opts.familyDir = argv[++i]
|
||
else positional.push(argv[i])
|
||
}
|
||
opts.pkgPath = positional[0]
|
||
return opts
|
||
}
|
||
|
||
async function main() {
|
||
const opts = parseArgs(process.argv.slice(2))
|
||
if (!opts.pkgPath) {
|
||
console.error('用法: node scripts/e2e-mount-rewrite <tarball package.json> [--root DIR] [--family-dir DIR]')
|
||
process.exit(1)
|
||
}
|
||
try {
|
||
await rewriteDependencies({ ...opts, log: line => console.log('[e2e-mount-rewrite]', line) })
|
||
} catch (error) {
|
||
console.error('[e2e-mount-rewrite]', error.message)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
if (require.main === module) main()
|
||
|
||
module.exports = {
|
||
rewriteDependencies,
|
||
findWorkspacePackage,
|
||
checkPublished,
|
||
resolvesFromPublished,
|
||
packWorkspace,
|
||
FAMILY_SCOPE,
|
||
}
|
||
|