mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-16 01:26:16 -08:00
test(toolchain): [MOD-01] evaluate isolated Bun installs and retain Yarn
This commit is contained in:
1 parent
ddf3d6ccbc
commit
18215b97c0
14 files changed
+2401
-5
No files matched your search
+2
-1
@@ -158,7 +158,8 @@
|
||||
"test:rollback:mainline": "node scripts/mainline-rehearsal.mjs",
|
||||
"check:rollback-inventory": "node refactor/scripts/rollback-inventory.mjs --check",
|
||||
"test:rollback:pages": "node scripts/pages-recovery.mjs",
|
||||
"test:rollback:pages:browser": "node test/pages-recovery-browser.mjs"
|
||||
"test:rollback:pages:browser": "node test/pages-recovery-browser.mjs",
|
||||
"test:bun-install": "node refactor/scripts/bun-evaluation.mjs"
|
||||
},
|
||||
"browserslist": "last 1 Chrome version",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| [逐包恢复清单](rollback-inventory.md) | 全包精确旧版本、依赖及完整回退产物的缺口 |
|
||||
| [工具链与发布](toolchain-release.md) | TypeScript、Bun、构建、版本管理和发布回退 |
|
||||
| [已实现的开发环境](toolchain-setup.md) | Node/Yarn 固定版本、锁文件、安装命令和实际验证范围 |
|
||||
| [Bun 隔离安装评估](bun-evaluation.md) | 固定版本对比、传递依赖/资源差异及继续使用 Yarn 的依据 |
|
||||
| [全包大版本策略](version-policy.md) | 每包分别升级一个 major 的目标清单、兼容要求和版本落地步骤 |
|
||||
| [已实现的 CI 入口](ci-setup.md) | Yarn 检查/构建、只读 lint、Pages 隔离及远端待验收状态 |
|
||||
| [GitHub CI/CD](github-ci-cd.md) | PR/兼容矩阵、构建报告、Pages、npm 发布及远端准入验证 |
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,100 @@
|
||||
# Bun 隔离安装评估
|
||||
|
||||
MOD-01 的结论是继续使用 Yarn Classic 1.22.22。Bun 1.4.2 的普通迁移可以安装、
|
||||
执行现有 Node 单元测试和完整构建,但首次冻结迁移失败,传递依赖及部分资源与
|
||||
Yarn 冻结基线不同。本评估不采用 Bun,不新增受维护的 bun.lock,也不替换运行时
|
||||
或 bundler。未来重测版本时重新建立证据,不把这次结论外推到其他 Bun 版本。
|
||||
|
||||
## 环境与来源
|
||||
|
||||
- Windows x64,Node 24.21.0,Yarn 1.22.22,Bun 1.4.2+744846f84。
|
||||
- 三份独立、仓库外的 Git detached checkout,同一提交
|
||||
`ddf3d6ccbc2fa7a3fa1a25c2b935b9d7045cee5c`,各有自己的 node_modules。
|
||||
- Bun 官方 [1.4.2 发布归档](https://github.com/oven-sh/bun/releases/tag/bun-v1.4.2)
|
||||
`bun-windows-x64.zip`:39,807,510 字节,SHA-256
|
||||
`ce4c17497b2f29712a99d3d53f028de28cd42e3bacb8589599e7f000e49b6405`。
|
||||
下载前记录 GitHub release asset digest,下载后比对,再解压到忽略的工具缓存。
|
||||
没有升级全局 Bun 1.3.14,也没有把二进制加入生产依赖。
|
||||
- 安装钩子正常启用;只排除 Playwright 浏览器下载,并清空 NODE_PATH。
|
||||
历史契约测试使用复制的冻结 releases 夹具,不复制已有 node_modules。
|
||||
- 初步在仓库内建的 checkout 可能借用祖先依赖,发现后保留日志,改用仓库外目录。
|
||||
正式 Yarn 安装复用初步运行的下载缓存;Bun 首次安装是冷缓存。因此耗时仅作
|
||||
执行记录,不能据此宣传安装速度差异。
|
||||
|
||||
## 结果
|
||||
|
||||
| 检查 | Yarn | Bun |
|
||||
| --- | --- | --- |
|
||||
| 从 yarn.lock 初次冻结安装 | 通过 | 失败:lockfile had changes, but lockfile is frozen |
|
||||
| 另一干净目录普通迁移 | 不适用 | 通过,生成仅存于评估目录的 bun.lock |
|
||||
| 生成 bun.lock 后冻结复装 | 不适用 | 通过,锁文件与完整审计依赖图未变 |
|
||||
| Node 单元测试 | 2,906 通过,0 跳过 | 2,906 通过,0 跳过 |
|
||||
| `yarn ci:build` | 通过 | 在 Bun 安装的依赖上通过,仍由 Yarn/Node 执行 |
|
||||
| `yarn typecheck` | 415 个 TS 生产文件及类型消费者通过 | 同样通过,含 TS 5.9.3 / 4.3.5 / 5.1.6 矩阵 |
|
||||
| 根工具依赖版本 | 与 manifest 固定版本一致 | 与 Yarn 一致 |
|
||||
| 依赖实例 / 唯一 name@version | 1,229 / 1,125 | 1,439 / 1,135 |
|
||||
| 审计的唯一原生/worker/WASM 资源 | 92 | 90 |
|
||||
| 未解析必需的非 peer 依赖 | 0 | 0 |
|
||||
|
||||
物理提升方式不同不直接判为不兼容。比较脚本另外按父包版本、依赖字段、请求范围
|
||||
和实际解析版本去重比较,保留未满足 peer/平台可选依赖。Bun 为旧搜索插件安装了
|
||||
flexsearch 0.7.43 peer;Yarn 对应位置解析到 0.8.158。Algolia 从 5.48.0 变为
|
||||
5.59.0;Vite 5.4.21 的 Rollup 从 4.57.1 变为 4.63.3。这些是实测解析差异,
|
||||
不表示所有依赖升级都是缺陷,也不能以单元测试通过替代依赖等价。
|
||||
|
||||
`bun pm untrusted` 明确列出 less 4.5.1 与 nx 20.8.4 的 postinstall 未运行。
|
||||
没有使用全量 trust 绕过差异。根 postinstall 中的 npx 被 Bun 执行为 bun x,
|
||||
Lerna 8.2.4 报告无 workspace prepare 脚本。两者的默认脚本策略不相同,参见
|
||||
[Bun lifecycle 文档](https://bun.com/docs/pm/lifecycle)。
|
||||
|
||||
完整构建包含 21 个库、核心声明/i18n、编辑器声明、站点资源、文档站及五项导入
|
||||
测试。对各包 dist/types 与 docs 的 compiled/document/assets-ts/assets-js
|
||||
核对精确字节:两边各 534 文件,448 文件相同;29 个同路径文件改变,另有两边
|
||||
各 57 个独有文件名,全部在 docs/document。库、声明与其余被检查输出相同。
|
||||
不做文本归一化,不把带不同哈希的站点文件名当作丢失;此对照也未单独排除所有
|
||||
文档构建非确定性因素,因此不把每个差异都归因于某个具体依赖。
|
||||
|
||||
安装资源审计覆盖依赖目录内的 .node/.exe/.wasm 与 worker/worklet JS 文件,
|
||||
不等于所有平台二进制或所有包资源。此任务没有进行 Bun 产物的浏览器、物理设备
|
||||
或 npm 发布验收;既已决定不采用,不建立它作为第二套受支持 CI 环境。
|
||||
|
||||
## 复跑与维护
|
||||
|
||||
先使用 `.node-version` 的 Node 与固定 Yarn。准备阶段读取 HEAD,不包含未提交
|
||||
代码;因此比较生产内容前应先提交对应任务。命令级固定 Git 换行设置,不改本机
|
||||
或仓库 Git 配置。准备命令会输出评估目录,并写入 `.cache/bun-evaluation/latest.json`。
|
||||
|
||||
```text
|
||||
yarn test:bun-install prepare
|
||||
yarn test:bun-install yarn <run-directory>
|
||||
yarn test:bun-install bun-frozen <run-directory> <bun-executable>
|
||||
yarn test:bun-install bun-migrate <run-directory> <bun-executable>
|
||||
node refactor/scripts/install-graph.mjs <yarn-checkout> <run-directory>/yarn-graph.json
|
||||
node refactor/scripts/install-graph.mjs <bun-migrate-checkout> <run-directory>/bun-graph.json
|
||||
node refactor/scripts/install-compare.mjs <run-directory>
|
||||
```
|
||||
|
||||
每阶段日志使用独占创建,不能覆盖后重试。初次安装要求无 node_modules;失败的
|
||||
冻结迁移和普通迁移使用不同 checkout。可通过 ARTPLAYER_EVAL_CACHE 显式复用
|
||||
评估目录下的缓存,脚本记录实际冷/热状态。各 checkout 内依次运行同一组
|
||||
`yarn test:unit`、`yarn ci:build`、`yarn typecheck`;需要历史归档时只复制
|
||||
`refactor/.cache/releases`,不要复制 node_modules。类型检查要在构建完成后运行。
|
||||
|
||||
```text
|
||||
node refactor/scripts/install-build-compare.mjs <run-directory>
|
||||
yarn test:bun-install bun-repeat <run-directory> <bun-executable>
|
||||
node refactor/scripts/install-graph.mjs <bun-migrate-checkout> <run-directory>/bun-repeat-graph.json
|
||||
node --test refactor/scripts/install-graph.test.mjs
|
||||
```
|
||||
|
||||
- bun-evaluation.mjs:固定版本、checkout/缓存边界、安装进程、锁文件保护与日志。
|
||||
- install-graph.mjs:真实依赖解析、资源指纹;拒绝逃出 checkout 的依赖链接。
|
||||
包名为 buffer/punycode 等 Node 内置同名 npm 包也纳入审计。
|
||||
- install-compare.mjs:语义依赖/peer/资源差异;不将提升路径不同当成版本变化。
|
||||
- install-build-compare.mjs:相同命令构建后的产物清单与逐字节差异。
|
||||
- install-graph.test.mjs:防止从父目录借依赖、同名内置模块漏报、嵌套解析与资源审计。
|
||||
|
||||
评估 checkout、缓存及原始失败报告保留在本机忽略目录,可能需要数 GB 空间。
|
||||
清理时先核对 snapshot.json 的 checkoutRoot、Git worktree 列表和实际绝对路径,
|
||||
使用 Git worktree 管理及原生文件操作;不能删除本仓库或其他任务的目录。
|
||||
正式证据索引见 [机器记录](baselines/bun-install-validation.json)。
|
||||
@@ -0,0 +1,33 @@
|
||||
# MOD-01 Bun 安装评估:保留 Yarn
|
||||
|
||||
已完成固定 Bun 1.4.2 的仓库外安装评估,结论是不采用。公开 API、生产源码、
|
||||
根 yarn.lock、Node 运行时与 bundler 均未改变;新增 test:bun-install 和四个职责
|
||||
独立的审计/比较脚本,模块地图与复跑步骤见 [维护文档](../bun-evaluation.md)。
|
||||
|
||||
## 实际验证
|
||||
|
||||
源提交 ddf3d6ccbc2fa7a3fa1a25c2b935b9d7045cee5c,Windows x64,Node 24.21.0、
|
||||
Yarn 1.22.22、Bun 1.4.2。首次 Bun 冻结迁移因锁变更失败,报告保留;另一个
|
||||
干净目录的普通迁移及生成锁后的冻结复装通过,后者依赖图/锁未变。
|
||||
|
||||
两边独立安装后均通过 2,906 项 Node 单元测试、415 个 TS 生产文件及公开/精确
|
||||
消费者检查、完整 ci:build(含 21 库、声明、i18n、文档及五项导入测试)。
|
||||
根固定工具相同,但 Algolia/Rollup 等传递解析、peer 与安装脚本策略不等价。
|
||||
资源审计 92 对 90 个;构建清单各 534 文件中 448 完全相同,29 同路径改变、
|
||||
两边各 57 独有路径均在文档站。库输出/声明相同不代替整套工具链等价。
|
||||
|
||||
新增两项审计回归通过,覆盖借用祖先 node_modules、内置同名 npm 包、嵌套依赖、
|
||||
可选 peer 与资源指纹。修正审计器把 buffer 等误当内置模块漏报的问题,之后重新
|
||||
生成两套图;不将修正前的包数量用于正式结论。scoped ESLint、严格工具链及计划
|
||||
检查通过。完整日志路径与 SHA-256 见 [机器证据](../baselines/bun-install-validation.json)。
|
||||
|
||||
## 边界与恢复
|
||||
|
||||
初步 checkout 位于仓库内,发现可能借用父目录依赖后改用 OS 临时目录;初步日志
|
||||
保留。正式 Yarn 复用下载缓存,Bun 初装为冷缓存,不提供速度提升比例。安装依赖
|
||||
钩子没有全部禁用,也没有对 Bun 屏蔽的 less/nx 钩子强制 trust。没有改变全局 Bun。
|
||||
|
||||
此任务只作安装采用决策,不声称 Bun 浏览器、跨平台、真机或 npm 发布验收通过。
|
||||
仍由 MOD-05 汇总工具链与性能决策。移除本任务脚本及文档即可撤销工程改动;
|
||||
隔离 checkout/缓存清理须按维护文档核对归属,不影响工作区现有 node_modules。
|
||||
任务独立本地提交后运行提交审计;未推送、部署或发布。
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 262 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 45 / doing 22 / blocked 0 / done 195 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 44 / doing 22 / blocked 0 / done 196 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| MOD-01 | workspace<br>Bun 固定版本干净安装试点 | ENG-09 | 独立目录的 Bun 安装与 Yarn 冻结基线对比,默认仍为用户选定的 Yarn | Node 测试仍通过;安装与资源一致才决定采用;不改 bundler;不自动替换默认 packageManager 或维护锁文件 | H | todo |
|
||||
| MOD-01 | workspace<br>Bun 固定版本干净安装试点 | ENG-09 | 独立目录的 Bun 安装与 Yarn 冻结基线对比,默认仍为用户选定的 Yarn | Node 测试仍通过;安装与资源一致才决定采用;不改 bundler;不自动替换默认 packageManager 或维护锁文件 | H | done |
|
||||
| MOD-PLUGIN-01 | workspace<br>迁移插件生成器与类型化模板 | ENG-06, SITE-03 | 保留 create:plugin CLI;纯渲染/文件发布边界、TS 工厂/声明/测试/示例及维护指南 | 防止覆盖现有包或示例;失败回退;生成包可构建并通过严格类型、旧模块消费及测试;不自动修改既有包或运行安装发布 | M | done |
|
||||
| MOD-02 | workspace<br>整理剩余开发/构建脚本与插件模板 | ENG-06, SITE-03, MOD-PLUGIN-01 | dev/build/utils/create-plugin 的 TS 与可测 CLI,模板同时提供旧 API | 旧脚本入口保留、新插件类型/测试/示例齐全,Lerna 改动单独取证 | M | done |
|
||||
| MOD-DEV-01 | workspace<br>修复开发服务器失败状态并明确资源生命周期 | MOD-02 | 修复 Servor 占用端口却退出 0 的已复现问题;明确 HTTP/监听/热刷新连接与定时器的启动、失败和关闭责任 | 保持默认 8082、docs 页面、自动刷新和旧 dev 命令;端口冲突退出非零且不终止其他服务,连续启动/停止及编译错误恢复有实际验证;跨平台监听差异单独留证 | M | done |
|
||||
@@ -653,6 +653,7 @@
|
||||
- SITE-07: [记录](site-inventory.md) [记录](baselines/site-provenance.json) [记录](changes/2026-09-14-SITE-07-vendor-notices.md) [记录](baselines/site-notices-checkpoint.json)
|
||||
- EX-01: [记录](changes/2026-09-14-EX-01-react-consumer.md) [记录](baselines/react-consumer-validation.json) [记录](scripts/react-consumer.mjs)
|
||||
- EX-02: [记录](changes/2026-09-14-EX-02-vue-consumer.md) [记录](baselines/vue-consumer-validation.json) [记录](scripts/vue-consumer.mjs)
|
||||
- MOD-01: [记录](changes/2026-09-15-MOD-01-bun-evaluation.md) [记录](baselines/bun-install-validation.json) [记录](bun-evaluation.md)
|
||||
- MOD-PLUGIN-01: [记录](changes/2026-09-14-MOD-PLUGIN-01-scaffold.md) [记录](baselines/scaffold-validation.json)
|
||||
- MOD-02: [记录](changes/2026-09-14-MOD-02-library-tooling.md) [记录](baselines/library-tooling-validation.json)
|
||||
- MOD-DEV-01: [记录](changes/2026-09-14-MOD-DEV-01-dev-server.md) [记录](baselines/dev-server-validation.json)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 进度与证据
|
||||
|
||||
## MOD-01 Bun 隔离安装评估完成,保留 Yarn
|
||||
|
||||
固定 Bun 1.4.2 与 Yarn 1.22.22 在仓库外同提交安装。Bun 初次冻结迁移失败,
|
||||
普通迁移与随后冻结复装通过;传递依赖和安装脚本策略仍与 Yarn 不同,因此不采用。
|
||||
两边 2,906 项单元测试、415 个 TS 源文件/类型消费者、完整构建均通过。库输出
|
||||
与声明一致,文档站存在精确字节差异;不把测试通过当成安装等价。新增可复跑脚本、
|
||||
审计负例与[记录](changes/2026-09-15-MOD-01-bun-evaluation.md)。默认仍是 Yarn,
|
||||
根锁不变,未切换运行时/bundler;262 项现为 196 done、22 doing、44 todo。
|
||||
|
||||
## REL-04 完整旧站点恢复检查点
|
||||
|
||||
修正 Git archive 的本机换行影响:原 ZIP 有 403 处 CRLF 差异,保留失败证据;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hash } from './releases.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('../../', import.meta.url))
|
||||
const parent = path.join(root, 'refactor/.cache/bun-evaluation')
|
||||
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
|
||||
assert.equal(process.versions.node, fs.readFileSync(path.join(root, '.node-version'), 'utf8').trim())
|
||||
const phase = process.argv[2]
|
||||
assert(['prepare', 'yarn', 'bun-frozen', 'bun-migrate', 'bun-repeat'].includes(phase), 'Use prepare, yarn, bun-frozen, bun-migrate or bun-repeat')
|
||||
fs.mkdirSync(parent, { recursive: true })
|
||||
|
||||
if (phase === 'prepare') {
|
||||
assert.equal(process.argv.length, 3)
|
||||
const output = fs.mkdtempSync(path.join(parent, 'run-'))
|
||||
const checkoutRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'artplayer-install-'))
|
||||
assert(!path.resolve(checkoutRoot).startsWith(path.resolve(root)), 'Install checkouts must stay outside the repository')
|
||||
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', windowsHide: true }).trim()
|
||||
const snapshot = { task: 'MOD-01', source, createdAt: new Date().toISOString(), node: process.versions.node, checkoutRoot, scope: 'Detached checkouts outside the repository with independent node_modules; default Yarn unchanged', rootFilesAtPreparation: {}, checkouts: {}, sourceFiles: {} }
|
||||
for (const file of ['package.json', 'yarn.lock']) snapshot.rootFilesAtPreparation[file] = hash(fs.readFileSync(path.join(root, file)))
|
||||
for (const name of ['yarn', 'bun-frozen', 'bun-migrate']) {
|
||||
const checkout = path.join(checkoutRoot, name)
|
||||
const log = execFileSync('git', ['-c', 'core.autocrlf=false', '-c', 'core.eol=lf', 'worktree', 'add', '--detach', checkout, source], { cwd: root, encoding: 'utf8', windowsHide: true })
|
||||
fs.writeFileSync(path.join(output, `${name}-checkout.log`), log)
|
||||
assert(!fs.existsSync(path.join(checkout, 'node_modules')))
|
||||
snapshot.checkouts[name] = checkout
|
||||
snapshot.sourceFiles[name] = Object.fromEntries(['package.json', 'yarn.lock'].map(file => [file, hash(fs.readFileSync(path.join(checkout, file)))]))
|
||||
}
|
||||
write(path.join(output, 'snapshot.json'), snapshot)
|
||||
write(path.join(parent, 'latest.json'), { output: path.relative(root, output).replaceAll('\\', '/') })
|
||||
console.log(`Prepared isolated install comparison: ${output}`)
|
||||
}
|
||||
else {
|
||||
const output = fs.realpathSync(path.resolve(root, process.argv[3] || ''))
|
||||
assert.equal(path.dirname(output), fs.realpathSync(parent), 'Use a direct evaluation directory')
|
||||
const snapshot = read(path.join(output, 'snapshot.json'))
|
||||
const checkoutPhase = phase === 'bun-repeat' ? 'bun-migrate' : phase
|
||||
const checkout = fs.realpathSync(snapshot.checkouts[checkoutPhase])
|
||||
assert.equal(path.dirname(checkout), fs.realpathSync(snapshot.checkoutRoot))
|
||||
assert(!checkout.startsWith(fs.realpathSync(root) + path.sep), 'Install checkout must not inherit the original node_modules')
|
||||
assert.equal(path.basename(checkout), checkoutPhase)
|
||||
if (phase === 'bun-repeat')
|
||||
assert(fs.existsSync(path.join(checkout, 'bun.lock')), 'Repeat requires the generated Bun lock')
|
||||
else
|
||||
assert(!fs.existsSync(path.join(checkout, 'node_modules')), 'Initial installation requires a clean checkout')
|
||||
const executable = phase === 'yarn' ? process.execPath : fs.realpathSync(process.argv[4] || '')
|
||||
const yarn = process.env.npm_execpath
|
||||
if (phase === 'yarn') {
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Run through pinned Yarn')
|
||||
assert(yarn && fs.existsSync(yarn))
|
||||
}
|
||||
else {
|
||||
assert.equal(execFileSync(executable, ['--version'], { encoding: 'utf8', windowsHide: true }).trim(), '1.4.2', 'This evaluation pins Bun 1.4.2')
|
||||
}
|
||||
const cache = process.env.ARTPLAYER_EVAL_CACHE ? fs.realpathSync(process.env.ARTPLAYER_EVAL_CACHE) : path.join(output, `${phase}-cache`)
|
||||
assert(path.resolve(cache).startsWith(fs.realpathSync(parent) + path.sep), 'Keep install caches inside the evaluation area')
|
||||
const coldPackageCache = !fs.existsSync(cache) || fs.readdirSync(cache).length === 0
|
||||
const args = phase === 'yarn'
|
||||
? [yarn, 'install', '--frozen-lockfile', '--non-interactive', '--cache-folder', cache]
|
||||
: ['install', ...(['bun-frozen', 'bun-repeat'].includes(phase) ? ['--frozen-lockfile'] : []), '--cache-dir', cache, '--backend=copyfile']
|
||||
const guardedFiles = ['package.json', 'yarn.lock', ...(phase === 'bun-repeat' ? ['bun.lock'] : [])]
|
||||
const identities = Object.fromEntries(guardedFiles.map(file => [file, hash(fs.readFileSync(path.join(checkout, file)))]))
|
||||
const start = performance.now()
|
||||
const log = fs.openSync(path.join(output, `${phase}-install.log`), 'wx')
|
||||
let result
|
||||
try {
|
||||
result = spawnSync(executable, args, { cwd: checkout, encoding: 'utf8', stdio: ['ignore', log, log], timeout: 600000, windowsHide: true, env: { ...process.env, NODE_PATH: '', PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1', BUN_TELEMETRY: '0' } })
|
||||
}
|
||||
finally { fs.closeSync(log) }
|
||||
const after = Object.fromEntries(Object.keys(identities).map(file => [file, hash(fs.readFileSync(path.join(checkout, file)))]))
|
||||
assert.deepEqual(after, identities, 'Evaluation modified source manifest or Yarn lock')
|
||||
const record = { task: 'MOD-01', phase, source: snapshot.source, node: process.versions.node, executable, args, durationMs: performance.now() - start, exitCode: result.status, signal: result.signal, error: result.error?.message, sourceFilesUnchanged: true, bunLock: fs.existsSync(path.join(checkout, 'bun.lock')) ? hash(fs.readFileSync(path.join(checkout, 'bun.lock'))) : null, log: `${phase}-install.log`, coldPackageCache, browserDownloadExcluded: true }
|
||||
write(path.join(output, `${phase}-install.json`), record)
|
||||
console.log(JSON.stringify(record))
|
||||
process.exitCode = result.status ?? 1
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { hash } from './releases.mjs'
|
||||
|
||||
const output = path.resolve(process.argv[2])
|
||||
const snapshot = JSON.parse(fs.readFileSync(path.join(output, 'snapshot.json'), 'utf8'))
|
||||
function inventory(root) {
|
||||
const files = {}
|
||||
function visit(relative) {
|
||||
const file = path.join(root, relative)
|
||||
const stat = fs.lstatSync(file)
|
||||
assert(!stat.isSymbolicLink(), `Unexpected generated link: ${relative}`)
|
||||
if (stat.isDirectory()) {
|
||||
for (const name of fs.readdirSync(file)) visit(`${relative}/${name}`)
|
||||
}
|
||||
else {
|
||||
files[relative] = { bytes: stat.size, sha256: hash(fs.readFileSync(file)) }
|
||||
}
|
||||
}
|
||||
for (const name of fs.readdirSync(path.join(root, 'packages'))) {
|
||||
for (const directory of ['dist', 'types']) {
|
||||
const relative = `packages/${name}/${directory}`
|
||||
if (fs.existsSync(path.join(root, relative)))
|
||||
visit(relative)
|
||||
}
|
||||
}
|
||||
for (const relative of ['docs/compiled', 'docs/document', 'docs/assets/ts', 'docs/assets/js']) visit(relative)
|
||||
return files
|
||||
}
|
||||
const yarn = inventory(snapshot.checkouts.yarn)
|
||||
const bun = inventory(snapshot.checkouts['bun-migrate'])
|
||||
const files = [...new Set([...Object.keys(yarn), ...Object.keys(bun)])].sort()
|
||||
const differences = files.filter(file => yarn[file]?.sha256 !== bun[file]?.sha256).map(file => ({ file, yarn: yarn[file] || null, bun: bun[file] || null }))
|
||||
const report = { task: 'MOD-01', source: snapshot.source, command: 'yarn ci:build under Node for both installed dependency sets', scope: 'All package dist/types and docs compiled/document/assets-ts/assets-js; exact bytes with no normalization', counts: { yarn: Object.keys(yarn).length, bun: Object.keys(bun).length, identical: files.length - differences.length }, differences, inventories: { yarn, bun } }
|
||||
fs.writeFileSync(path.join(output, 'build-comparison.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ counts: report.counts, differences: differences.map(item => item.file) }, null, 2))
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
const directory = path.resolve(process.argv[2])
|
||||
const read = name => JSON.parse(fs.readFileSync(path.join(directory, name), 'utf8'))
|
||||
const yarn = read('yarn-graph.json')
|
||||
const bun = read('bun-graph.json')
|
||||
const unique = values => [...new Set(values)].sort()
|
||||
function differences(left, right) {
|
||||
const a = new Set(left)
|
||||
const b = new Set(right)
|
||||
return { yarnOnly: [...a].filter(value => !b.has(value)).sort(), bunOnly: [...b].filter(value => !a.has(value)).sort() }
|
||||
}
|
||||
const tools = graph => Object.fromEntries(graph.rootTools.map(edge => [edge.name, edge.resolved]))
|
||||
const leftTools = tools(yarn)
|
||||
const rightTools = tools(bun)
|
||||
assert.deepEqual(Object.keys(leftTools).sort(), Object.keys(rightTools).sort())
|
||||
const rootToolChanges = Object.keys(leftTools).filter(name => leftTools[name] !== rightTools[name]).map(name => ({ name, yarn: leftTools[name], bun: rightTools[name] }))
|
||||
const edges = graph => unique(graph.requests.map(edge => `${edge.parent} ${edge.field} ${edge.name}@${edge.range} -> ${edge.resolved}`))
|
||||
const resources = graph => unique(graph.resources.map(item => `${item.package} ${item.path} ${item.bytes} ${item.sha256}`))
|
||||
const missing = graph => unique(graph.missing.map(edge => `${edge.parent} ${edge.field} ${edge.name}@${edge.range} optional=${edge.optional}`))
|
||||
const report = {
|
||||
task: 'MOD-01',
|
||||
source: read('snapshot.json').source,
|
||||
counts: Object.fromEntries([['yarn', yarn], ['bun', bun]].map(([name, graph]) => [name, { packageInstances: graph.packages.length, uniquePackages: unique(graph.packages.map(pkg => `${pkg.name}@${pkg.version}`)).length, resolvedEdges: graph.requests.length, uniqueResources: resources(graph).length }])),
|
||||
rootToolChanges,
|
||||
packages: differences(yarn.packages.map(pkg => `${pkg.name}@${pkg.version}`), bun.packages.map(pkg => `${pkg.name}@${pkg.version}`)),
|
||||
requests: differences(edges(yarn), edges(bun)),
|
||||
missing: differences(missing(yarn), missing(bun)),
|
||||
resources: differences(resources(yarn), resources(bun)),
|
||||
missingRequired: Object.fromEntries([['yarn', yarn], ['bun', bun]].map(([name, graph]) => [name, graph.missing.filter(edge => !edge.optional && edge.field !== 'peerDependencies')])),
|
||||
note: 'Physical hoisting differences are excluded from semantic comparisons. Missing optional/platform and peer requests remain explicit; this report alone does not determine build compatibility.',
|
||||
}
|
||||
fs.writeFileSync(path.join(directory, 'comparison.json'), `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(JSON.stringify({ counts: report.counts, rootToolChanges, resources: report.resources, missingRequired: report.missingRequired }, null, 2))
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { hash } from './releases.mjs'
|
||||
|
||||
export function installedGraph(checkout) {
|
||||
const root = fs.realpathSync(checkout)
|
||||
const packages = new Map()
|
||||
const requests = []
|
||||
const missing = []
|
||||
function manifestAt(location) {
|
||||
const actual = fs.realpathSync(location)
|
||||
assert(actual.startsWith(root + path.sep), 'Installed dependency escaped isolated checkout')
|
||||
return { directory: actual, manifest: JSON.parse(fs.readFileSync(path.join(actual, 'package.json'), 'utf8')) }
|
||||
}
|
||||
function resolve(from, name) {
|
||||
const require = createRequire(path.join(from, 'package.json'))
|
||||
// Audit npm packages even when their names also identify Node builtins.
|
||||
for (const location of require.resolve.paths('artplayer-install-audit-probe')) {
|
||||
const directory = path.join(location, name)
|
||||
if (fs.existsSync(path.join(directory, 'package.json')))
|
||||
return manifestAt(directory)
|
||||
}
|
||||
return null
|
||||
}
|
||||
function visit(directory, manifest, workspace = false) {
|
||||
const key = path.relative(root, directory).replaceAll('\\', '/') || '.'
|
||||
if (packages.has(key))
|
||||
return
|
||||
packages.set(key, { path: key, name: manifest.name, version: manifest.version, license: manifest.license || null, workspace, lifecycle: Object.fromEntries(Object.entries(manifest.scripts || {}).filter(([name]) => ['preinstall', 'install', 'postinstall', 'prepare'].includes(name))) })
|
||||
const fields = workspace ? ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] : ['dependencies', 'optionalDependencies', 'peerDependencies']
|
||||
for (const field of fields) {
|
||||
for (const [name, range] of Object.entries(manifest[field] || {})) {
|
||||
const dependency = resolve(directory, name)
|
||||
const edge = { from: key, parent: `${manifest.name}@${manifest.version}`, field, name, range, optional: field === 'optionalDependencies' || (field === 'peerDependencies' && manifest.peerDependenciesMeta?.[name]?.optional === true) }
|
||||
if (!dependency) {
|
||||
missing.push(edge)
|
||||
continue
|
||||
}
|
||||
requests.push({ ...edge, resolved: `${dependency.manifest.name}@${dependency.manifest.version}`, to: path.relative(root, dependency.directory).replaceAll('\\', '/') })
|
||||
visit(dependency.directory, dependency.manifest)
|
||||
}
|
||||
}
|
||||
}
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
||||
visit(root, manifest, true)
|
||||
for (const name of fs.readdirSync(path.join(root, 'packages'))) {
|
||||
const directory = path.join(root, 'packages', name)
|
||||
if (fs.existsSync(path.join(directory, 'package.json'))) {
|
||||
const entry = manifestAt(directory)
|
||||
// A workspace first reached through a runtime dependency still needs its dev/peer audit.
|
||||
packages.delete(path.relative(root, directory).replaceAll('\\', '/'))
|
||||
visit(directory, entry.manifest, true)
|
||||
}
|
||||
}
|
||||
const resources = []
|
||||
for (const pkg of packages.values()) {
|
||||
if (pkg.workspace)
|
||||
continue
|
||||
const directory = path.join(root, pkg.path)
|
||||
function walk(folder) {
|
||||
for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
|
||||
if (entry.name === 'node_modules' || entry.isSymbolicLink())
|
||||
continue
|
||||
const file = path.join(folder, entry.name)
|
||||
if (entry.isDirectory())
|
||||
walk(file)
|
||||
else if (entry.isFile() && /\.(?:node|wasm|exe)$|(?:worker|worklet)[^/\\]*\.[cm]?js$/i.test(entry.name))
|
||||
resources.push({ package: `${pkg.name}@${pkg.version}`, path: path.relative(directory, file).replaceAll('\\', '/'), bytes: fs.statSync(file).size, sha256: hash(fs.readFileSync(file)) })
|
||||
}
|
||||
}
|
||||
walk(directory)
|
||||
}
|
||||
return { node: process.versions.node, root, packages: [...packages.values()].sort((a, b) => a.path.localeCompare(b.path)), requests, missing, resources, rootTools: requests.filter(edge => edge.from === '.' && edge.field === 'devDependencies') }
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
assert.equal(process.argv.length, 4, 'Use <checkout> <report.json>')
|
||||
const graph = installedGraph(process.argv[2])
|
||||
fs.writeFileSync(process.argv[3], `${JSON.stringify(graph, null, 2)}\n`)
|
||||
console.log(`Installed graph: ${graph.packages.length} package instances, ${graph.requests.length} resolved edges, ${graph.resources.length} native/worker/WASM resources; ${graph.missing.length} absent optional/peer/required requests retained`)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify install comparison cannot borrow ancestor dependencies.
|
||||
import test from 'node:test'
|
||||
import { consumerDirectory, removeConsumer } from '../../scripts/package-consumer.mjs'
|
||||
import { installedGraph } from './install-graph.mjs'
|
||||
|
||||
function write(directory, manifest) {
|
||||
fs.mkdirSync(directory, { recursive: true })
|
||||
fs.writeFileSync(path.join(directory, 'package.json'), JSON.stringify(manifest))
|
||||
}
|
||||
|
||||
test('installation audit rejects a dependency resolved from the parent checkout', () => {
|
||||
const directory = consumerDirectory()
|
||||
try {
|
||||
const root = path.join(directory, 'checkout')
|
||||
write(root, { name: 'root', version: '1.0.0', dependencies: { borrowed: '1.0.0' } })
|
||||
fs.mkdirSync(path.join(root, 'packages'))
|
||||
write(path.join(directory, 'node_modules/borrowed'), { name: 'borrowed', version: '1.0.0' })
|
||||
assert.throws(() => installedGraph(root), /escaped isolated checkout/)
|
||||
}
|
||||
finally { removeConsumer(directory) }
|
||||
})
|
||||
|
||||
test('installation audit records workspace peers, nested resolution and resource identities', () => {
|
||||
const directory = consumerDirectory()
|
||||
try {
|
||||
write(directory, { name: 'root', version: '1.0.0', devDependencies: { compiler: '1.0.0' }, dependencies: { buffer: '5.7.1' } })
|
||||
write(path.join(directory, 'node_modules/buffer'), { name: 'buffer', version: '5.7.1' })
|
||||
write(path.join(directory, 'node_modules/compiler'), { name: 'compiler', version: '1.0.0', dependencies: { engine: '2.0.0' }, scripts: { postinstall: 'node install.js' } })
|
||||
write(path.join(directory, 'node_modules/compiler/node_modules/engine'), { name: 'engine', version: '2.0.0' })
|
||||
fs.writeFileSync(path.join(directory, 'node_modules/compiler/node_modules/engine/code.wasm'), 'fixture')
|
||||
write(path.join(directory, 'packages/plugin'), { name: 'plugin', version: '1.0.0', peerDependencies: { host: '^1' }, peerDependenciesMeta: { host: { optional: true } } })
|
||||
const graph = installedGraph(directory)
|
||||
assert.equal(graph.rootTools[0].resolved, 'compiler@1.0.0')
|
||||
assert.equal(graph.requests.find(item => item.name === 'buffer').resolved, 'buffer@5.7.1')
|
||||
assert.equal(graph.requests.find(item => item.name === 'engine').resolved, 'engine@2.0.0')
|
||||
assert.equal(graph.missing[0].name, 'host')
|
||||
assert.equal(graph.missing[0].optional, true)
|
||||
assert.equal(graph.resources.length, 1)
|
||||
assert.equal(graph.resources[0].package, 'engine@2.0.0')
|
||||
assert.equal(graph.resources[0].bytes, 7)
|
||||
assert.equal(graph.packages.find(item => item.name === 'compiler').lifecycle.postinstall, 'node install.js')
|
||||
}
|
||||
finally { removeConsumer(directory) }
|
||||
})
|
||||
+6
-2
@@ -4544,11 +4544,15 @@
|
||||
"dependsOn": [
|
||||
"ENG-09"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "独立目录的 Bun 安装与 Yarn 冻结基线对比,默认仍为用户选定的 Yarn",
|
||||
"acceptance": "Node 测试仍通过;安装与资源一致才决定采用;不改 bundler;不自动替换默认 packageManager 或维护锁文件",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-15-MOD-01-bun-evaluation.md",
|
||||
"baselines/bun-install-validation.json",
|
||||
"bun-evaluation.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "MOD-PLUGIN-01",
|
||||
|
||||
@@ -32,6 +32,10 @@ TypeScript 用于自有源码、检查与声明生成;Vite/Rollup 保留现有
|
||||
|
||||
## Bun 试点
|
||||
|
||||
MOD-01 已完成固定版本隔离评估,结论为保留 Yarn。Bun 1.4.2 普通迁移后 Node
|
||||
测试/类型/完整构建通过,但冻结迁移、传递依赖和钩子策略不等价;详见
|
||||
[实测结论与复跑](bun-evaluation.md)。以下是评估标准,不表示已采用 Bun。
|
||||
|
||||
| 步骤 | 检查 |
|
||||
| --- | --- |
|
||||
| 依赖基线 | 保存当前依赖和工作区行为,明确唯一锁文件与版本 pin;初次固定锁不夹带大规模升级 |
|
||||
|
||||
Reference in new issue
Block a user