`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.
102 lines
3.9 KiB
JavaScript
102 lines
3.9 KiB
JavaScript
/**
|
||
* 打包完整性:`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(', ')}`,
|
||
)
|
||
})
|