fix(packaging): ship lib/embed/, without which the published tarball cannot load

`package.json` declared

    "files": ["lib/*.js", "lib/loop.js", "lib/loop/*.js", "lib/types/*.d.ts"]

`lib/*.js` matches ONE level. `lib/embed/http.js` therefore never entered the
tarball, while `lib/index.js:35` imports it:

    import { createHttpEmbedder } from './embed/http.js'

## How this survived

Every test in this repo runs from the source tree, where the file obviously
exists. `npm pack --dry-run` printed "10 files" — a plausible number nobody had
reason to read through. The defect is only reachable from the one path that
ships: installing the tarball.

Measured, extracting the tarball and importing it:

    IMPORT FAILED: ERR_MODULE_NOT_FOUND
    Cannot find module '.../package/lib/embed/http.js'
      imported from '.../package/lib/index.js'

After the fix, the same command:

    IMPORT OK, exports: StableMem,apply,default,defaultPath,inject,instanceKey,lastUserText,name

## Fix

`lib/embed/*.js` added to the allowlist. The tarball goes from 10 to 11 files
(86.1 kB) and `lib/embed/http.js` is in it.

## Criterion

`test/packaging.test.js`:

1. every `.js` under `lib/` must be matched by at least one `files` pattern.
   The patterns compile to regexes the way npm treats them (`*` does not cross
   `/`), so the criterion needs no hand-maintained file list and cannot drift.
2. every relative import in `lib/index.js` must resolve on disk — otherwise
   criterion 1 could pass vacuously for a file that does not exist.

Reverting `files` to the old list turns criterion 1 red:

    not ok 1 - packaging: every file under lib/ is covered by package.json "files"
    these files live under lib/ but no "files" pattern reaches them ...
      lib/embed/http.js

## Why it belongs in this branch

V6 made the embedder assembly load-bearing (two instances, one per side). A
release that cannot import `lib/embed/http.js` has no embedder at all, so the
distinction V6 introduced would never reach a user. The bug predates this
branch; its blast radius is the shipped artifact.

Gates: npm test 294/294 (0 skipped), npm run check 12/12, npm pack 11 files.
This commit is contained in:
2026-11-05 11:38:56 +08:00
parent c97775bd18
commit 68775d327f
2 changed files with 103 additions and 1 deletions

View File

@ -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": {

101
test/packaging.test.js Normal file
View File

@ -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(', ')}`,
)
})