diff --git a/01_content/package.json b/01_content/package.json index a361cbb..86245e7 100644 --- a/01_content/package.json +++ b/01_content/package.json @@ -33,7 +33,8 @@ }, "scripts": { "build": "node build.mjs", - "check": "node --check lib/index.js && node --check lib/client.js" + "check": "node --check lib/index.js && node --check lib/client.js", + "pack": "node release-prep.mjs" }, "peerDependencies": { "@deepseek-ai/cordis": "*", diff --git a/01_content/release-prep.mjs b/01_content/release-prep.mjs index eb8b5f7..72c7a48 100644 --- a/01_content/release-prep.mjs +++ b/01_content/release-prep.mjs @@ -8,9 +8,8 @@ * - 绝不执行:git 操作 / push / tag / gh release / 任何对外发布动作。 * - 任何断言失败 = 进程以非零码退出,绝不输出"验收通过"。 * - * 用法(脚本以自身位置解析 01_content,不依赖执行 cwd): + * 用法(脚本以自身位置解析 01_content,不依赖执行 cwd;任何目录都可调用): * node release-prep.mjs - * node release-prep.mjs --skip-build # 仅重新验收现有 lib(排障用) */ import { spawnSync } from 'node:child_process' import { createHash } from 'node:crypto' @@ -20,7 +19,6 @@ import { fileURLToPath } from 'node:url' import { tmpdir } from 'node:os' const HERE = dirname(fileURLToPath(import.meta.url)) // 固定 = 01_content,绝不依赖执行 cwd -const SKIP_BUILD = process.argv.includes('--skip-build') // Windows 下 spawnSync('npm') 有 ENOENT、spawnSync('npm.cmd') 有 EINVAL 问题。 // 统一通过 node 的 npm-cli.js 调用(node 自带 npm 前缀,跨平台稳定)。 const { execPath } = await import('node:process') @@ -50,10 +48,9 @@ const VERSION = pkg.version console.log('[release-prep] version = ' + VERSION) // ---------- 2. 构建 + 语法检查 ---------- -if (!SKIP_BUILD) { - const build = spawnSync(process.execPath, [join(HERE, 'build.mjs')], { stdio: 'inherit' }) - if (build.status !== 0) { console.error('[release-prep] FAIL: build failed'); process.exit(1) } -} +// 构建必带 cwd: HERE(脚本自身位置=01_content),否则从仓库根调用会输出到根 lib/ 造成旧包 +const build = spawnSync(process.execPath, [join(HERE, 'build.mjs')], { stdio: 'inherit', cwd: HERE }) +if (build.status !== 0) { console.error('[release-prep] FAIL: build failed'); process.exit(1) } for (const f of ['lib/index.js', 'lib/client.js']) { const chk = spawnSync(process.execPath, ['--check', join(HERE, f)], { stdio: 'pipe' }) if (chk.status !== 0) { console.error('[release-prep] FAIL: node --check ' + f); process.exit(1) } @@ -102,7 +99,28 @@ mkdirSync(inst, { recursive: true }) writeFileSync(join(inst, 'package.json'), JSON.stringify({ name: 'wt-prep-test', version: '0.0.0', private: true })) const ins = runNpm(['install', tgz, '--no-audit', '--no-fund', '--loglevel=error'], { cwd: inst }) if (ins.status !== 0) { console.error('[release-prep] FAIL: npm install'); console.error(ins.stderr); process.exit(1) } -const imp = spawnSync(process.execPath, ['--input-type=module', '-e', "const m = await import('dsh-worktable'); const k = Object.keys(m); if (!['apply','inject','name','HEALTH_PATH'].every(x => k.includes(x))) { console.error('missing exports: ' + k.join(',')); process.exit(3) }"], { cwd: inst, stdio: 'pipe', encoding: 'utf8' }) +// import 断言脚本(写临时文件,避免 -e 多行转义):导出名称 + 类型 + 内容 + 包内版本文件 +const assertJs = join(inst, 'assert.mjs') +writeFileSync(assertJs, [ + "import { readFileSync } from 'node:fs'", + "const TARGET_VERSION = " + JSON.stringify(VERSION), + "const m = await import('dsh-worktable')", + "const fails = []", + "if (typeof m.apply !== 'function') fails.push('apply not function: ' + typeof m.apply)", + "if (!Array.isArray(m.inject)) fails.push('inject not array')", + "if (m.name !== 'dsh-worktable') fails.push('name mismatch: ' + m.name)", + "if (m.HEALTH_PATH !== '/api/worktable/health') fails.push('HEALTH_PATH mismatch: ' + m.HEALTH_PATH)", + "const pkg = JSON.parse(readFileSync('./node_modules/dsh-worktable/package.json', 'utf8'))", + "if (pkg.version !== TARGET_VERSION) fails.push('installed version mismatch: ' + pkg.version)", + "const idx = readFileSync('./node_modules/dsh-worktable/lib/index.js', 'utf8')", + "const cli = readFileSync('./node_modules/dsh-worktable/lib/client.js', 'utf8')", + "if (!idx.includes(JSON.stringify(TARGET_VERSION))) fails.push('index.js missing target version')", + "if (!cli.includes(JSON.stringify(TARGET_VERSION))) fails.push('client.js missing target version')", + "if (fails.length) { console.error(fails.join(String.fromCharCode(10))); process.exit(3) }", + "console.log('assert ok: apply/inject/name/HEALTH_PATH + versions(' + TARGET_VERSION + ')')", +].join('\n')) +const assertUrl = 'file:///' + assertJs.replace(/\\/g, '/') +const imp = spawnSync(process.execPath, ['--input-type=module', '--eval', 'await import(' + JSON.stringify(assertUrl) + ')'], { cwd: inst, stdio: 'pipe', encoding: 'utf8' }) if (imp.status !== 0) { console.error('[release-prep] FAIL: import() assertion'); console.error(imp.stderr); process.exit(1) } console.log('[release-prep] install + import assertion passed') diff --git a/AGENTS.md b/AGENTS.md index ab98bf8..d96e96c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,11 @@ node --check lib/index.js - 变更视图状态结构时同步更新 PRD 的持久化说明。 - **构建必须 `cd 01_content` 后执行**:误在仓库根跑会把 lib 写到仓库根 `lib/`,宿主仍加载 `01_content/lib` 旧 bundle,出现「改完不生效」假象(已有教训,见工作日志)。 +- **发布打包唯一入口 = `npm run pack`(01_content/release-prep.mjs)**:版本一致性 → 构建(cwd 固定 01_content)→ + node --check → npm pack → 结构清单断言(package/ 前缀 + 精确 7 文件,无 src/、无 .map)→ + 独立临时目录 npm install + import() 断言(apply 函数/inject 数组/name/HEALTH_PATH/包内双 bundle 版本) + → 双资产同源复制 + SHA-256。脚本零 git/gh 动作,发布上传由 gh 手动完成并下载远端比对。 + **发布禁止裸 npm pack 或手工 tar 生成发布包**;脚本从仓库任意目录调用均安全(以自身位置解析)。 ## 领域约定(会话中必须遵守)