diff --git a/package.json b/package.json index f5f881f..ccaa72d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "files": [ "lib/*.js", + "lib/embed/*.js", "lib/loop.js", "lib/loop/*.js", "lib/types/*.d.ts" @@ -18,7 +19,7 @@ "node": ">=22.5.0" }, "scripts": { - "test": "node --test test/store.test.js test/plugin.test.js test/loader.test.js test/phase-b-a.test.js test/phase-b-g.test.js test/phase-b-g-static.test.js test/phase-b-h.test.js test/phase-b-p1-vector-wiring.test.js test/phase-b-c4-centering.test.js test/phase-b-c5-reasons.test.js test/phase-b-v6-sides.test.js test/memory-log.test.js test/loop.test.js test/loop-ports.test.js test/loop-system-e2e.test.js", + "test": "node --test test/store.test.js test/packaging.test.js test/plugin.test.js test/loader.test.js test/phase-b-a.test.js test/phase-b-g.test.js test/phase-b-g-static.test.js test/phase-b-h.test.js test/phase-b-p1-vector-wiring.test.js test/phase-b-c4-centering.test.js test/phase-b-c5-reasons.test.js test/phase-b-v6-sides.test.js test/memory-log.test.js test/loop.test.js test/loop-ports.test.js test/loop-system-e2e.test.js", "check": "node scripts/checkpoints.js" }, "peerDependencies": { diff --git a/test/packaging.test.js b/test/packaging.test.js new file mode 100644 index 0000000..e211069 --- /dev/null +++ b/test/packaging.test.js @@ -0,0 +1,101 @@ +/** + * 打包完整性:`npm pack` 的产物必须能真的被加载。 + * + * ## 这条判据为什么存在 + * + * `package.json` 的 `files` 原本是 + * + * ["lib/*.js", "lib/loop.js", "lib/loop/*.js", "lib/types/*.d.ts"] + * + * 而 `lib/*.js` 只匹配 lib/ 下一层。于是 `lib/embed/http.js` —— `lib/index.js` + * 第 35 行 `import { createHttpEmbedder } from './embed/http.js'` 的目标 —— + * 从来没有被打进包。 + * + * 这个缺陷活下来的原因很具体:`npm pack --dry-run` 一直打印「10 files」, + * 数字看起来正常,没有人去读那十个文件名;而仓库里所有测试都是从源码树跑 + * 的,源码树里那个文件当然存在。**只有在「从 tarball 安装」这条路径上才会 + * 暴露**,而那正是发布出去的那条路径。 + * + * 实测(修复前,把 tarball 解开后 import): + * + * IMPORT FAILED: ERR_MODULE_NOT_FOUND + * Cannot find module '.../package/lib/embed/http.js' imported from '.../package/lib/index.js' + * + * 修好之后同一条命令:`IMPORT OK, exports: StableMem,apply,default,...` + * + * ## 判据形态 + * + * 静态、无副作用、不跑 npm:把 `files` 里的每个模式编译成正则,然后要求 + * **lib/ 下的每一个 .js 都至少被一个模式匹配**。运行时需要的文件全在 lib/ 下, + * 所以这条覆盖了「漏文件」这一类,不需要维护一份白名单。 + * + * 另加一条:`lib/index.js` 里所有相对 import 的目标必须存在于源码树 —— + * 否则上面那条可能在「文件根本不存在」时也绿(模式匹配不到不存在的文件)。 + * + * @module dsh-memory/test/packaging + */ + +import assert from 'node:assert/strict' +import test from 'node:test' +import { readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' + +const ROOT = new URL('..', import.meta.url) + +/** 把 npm `files` 里的一条模式编译成正则(`*` 不跨目录,与 npm 一致)。 */ +function patternToRegExp(pattern) { + const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*') + return new RegExp(`^${escaped}$`) +} + +/** lib/ 下所有 .js 文件的相对路径。 */ +function libFiles() { + const out = [] + const walk = (relative) => { + for (const entry of readdirSync(new URL(relative + '/', ROOT), { withFileTypes: true })) { + const path = `${relative}/${entry.name}` + if (entry.isDirectory()) walk(path) + else if (entry.name.endsWith('.js')) out.push(path) + } + } + walk('lib') + return out.sort() +} + +test('packaging: every file under lib/ is covered by package.json "files"', () => { + const pkg = JSON.parse(readFileSync(new URL('package.json', ROOT), 'utf8')) + assert.ok(Array.isArray(pkg.files), 'package.json must declare a "files" allowlist') + const patterns = pkg.files.map(patternToRegExp) + + const missing = libFiles().filter((path) => !patterns.some((re) => re.test(path))) + assert.deepEqual( + missing, + [], + 'these files live under lib/ but no "files" pattern reaches them, so they will be ' + + 'absent from the published tarball. `lib/*.js` matches ONE level only — a module ' + + 'in a subdirectory needs its own pattern. This is how lib/embed/http.js went ' + + 'missing while `npm pack --dry-run` happily reported a count:\n ' + + missing.join('\n '), + ) +}) + +test('packaging: every relative import in lib/index.js resolves on disk', () => { + const source = readFileSync(new URL('lib/index.js', ROOT), 'utf8') + const specifiers = [...source.matchAll(/from\s+'(\.[^']+)'/g)].map((m) => m[1]) + assert.ok(specifiers.length > 0, 'precondition: lib/index.js must have relative imports') + + const unresolved = specifiers.filter((specifier) => { + try { + const url = new URL(specifier, new URL('lib/index.js', ROOT)) + readFileSync(url) + return false + } catch { + return true + } + }) + assert.deepEqual( + unresolved, + [], + `lib/index.js imports modules that do not exist: ${unresolved.join(', ')}`, + ) +})