Files
HologramAgent-DSH-Memory-Port/test/packaging.test.js
jianf 68775d327f 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.
2026-11-05 11:38:56 +08:00

102 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 打包完整性:`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(', ')}`,
)
})