Phase 2 of docs/modules/rust/PLAN.md. Phase 1 built five guards and ran them by hand; this repo had no workflows at all, so nothing gated the branch that gets released and there was no way to release it. Three pieces: - **release.yml** — the derived-version engine link, installer and Module-uo already run (conventional-commit subjects since the newest tag; module.json's version survives as a floor; workflow_dispatch as the backdoor), assembling the bundle from an include list and publishing the tarball, the install manifest carrying its sha256, and SHA256SUMS. The tag is the number that ships and CI stamps it into the bundle's own module.json. - **pr-checks.yml** — server tests, check:imports, check:bundle, check:swagger, the client build, client tests and check:externals, plus frozen-manifest. - **frozen-manifest** — clones core at the sha pinned in ci/core-ref.json, generates its route table without this module and with it, and takes the difference. It ran locally against that exact ref: six routes, all documented, no core route moved. That is the first proof by a running core that /rust collides with nothing — phase 1 could only check it by reading, because core mounts /status and /version at a tier root where the loader's own collision probe cannot see them. The bundle carries no node_modules, because the shipped half declares no runtime dependencies (org lead, phase 2). checkBundle.js holds both halves of that: the include list still covers everything server/index.js reaches, and no dependency has appeared without the release learning to pack it. Verified by breaking it — dropping "model" from the list names the exact edit and exits 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
274 lines
9.6 KiB
JavaScript
274 lines
9.6 KiB
JavaScript
// The bundle check, checked.
|
|
//
|
|
// `scripts/checkBundle.js` exists because of a failure this module has not had
|
|
// and does not intend to: Module-uo's v1.0.0 shipped without `server/commands/`
|
|
// and died at the register stage on the operator's box. A check written in
|
|
// response to one bug is worth exactly as much as its coverage of that bug, so
|
|
// the first two tests below are that bug, in both modes — a list that has stopped
|
|
// covering what the entry point reaches, and a tarball with the file missing from
|
|
// it — and the third pair is this module's own version of it, a runtime
|
|
// dependency declared and not packed.
|
|
//
|
|
// **Every fixture is a template literal, and that is load-bearing** — the same
|
|
// reason checkImports.test.js gives. `scripts/checkImports.js` scans this
|
|
// directory too, so an ordinary quoted string holding a relative require would
|
|
// make this file fail that check. Templates are blanked by the stripper.
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
const fs = require('node:fs')
|
|
const os = require('node:os')
|
|
const path = require('node:path')
|
|
|
|
const {
|
|
reachable,
|
|
resolveFile,
|
|
checkDeclaration,
|
|
checkBundle,
|
|
declaredServerPaths,
|
|
runtimeDependencies,
|
|
} = require('../scripts/checkBundle')
|
|
|
|
/**
|
|
* Write a throwaway module tree: `files` under server/, `bundle` as its
|
|
* ci/bundle.json, `pkg` as its server/package.json. Returns the module root.
|
|
*/
|
|
function fixture(files, bundle = { server: ['index.js'] }, pkg = null) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'module-rust-bundle-'))
|
|
for (const [name, source] of Object.entries(files)) {
|
|
const file = path.join(root, 'server', name)
|
|
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
fs.writeFileSync(file, source)
|
|
}
|
|
fs.mkdirSync(path.join(root, 'ci'), { recursive: true })
|
|
fs.writeFileSync(path.join(root, 'ci', 'bundle.json'), JSON.stringify(bundle))
|
|
if (pkg) {
|
|
fs.mkdirSync(path.join(root, 'server'), { recursive: true })
|
|
fs.writeFileSync(path.join(root, 'server', 'package.json'), JSON.stringify(pkg))
|
|
}
|
|
return root
|
|
}
|
|
|
|
const cleanup = (root) => fs.rmSync(root, { recursive: true, force: true })
|
|
|
|
// ── The regression this script was written for ─────────────────────────────
|
|
|
|
test('--check catches a directory the include list has stopped covering', () => {
|
|
const root = fixture(
|
|
{
|
|
'index.js': `const r = require('./router/public/rust.router')`,
|
|
'router/public/rust.router.js': `module.exports = {}`,
|
|
},
|
|
{ server: ['index.js'] }, // `router` missing — exactly Module-uo's v1.0.0
|
|
)
|
|
try {
|
|
const { uncovered } = checkDeclaration(root)
|
|
assert.strictEqual(uncovered.size, 1)
|
|
assert.ok(uncovered.has('server/router'))
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('--bundle catches the file missing from an assembled tarball', () => {
|
|
const root = fixture({ 'index.js': `require('./router/public/rust.router')` })
|
|
try {
|
|
const { missing } = checkBundle(root)
|
|
assert.strictEqual(missing.length, 1)
|
|
assert.strictEqual(missing[0].specifier, './router/public/rust.router')
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
// ── This module's own version of that failure ──────────────────────────────
|
|
//
|
|
// The release packs no node_modules because the shipped half declares no
|
|
// dependencies (org lead, phase 2). The value of that decision is entirely in
|
|
// the day it stops being true being a LOUD day, so both modes ask.
|
|
|
|
test('--check reports a runtime dependency the release would not pack', () => {
|
|
const root = fixture({ 'index.js': `module.exports = 1` }, { server: ['index.js'] }, {
|
|
name: 'x',
|
|
dependencies: { ws: '^8.21.0' },
|
|
})
|
|
try {
|
|
assert.deepStrictEqual(checkDeclaration(root).dependencies, ['ws'])
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('--bundle reports a dependency the assembled bundle declares and does not carry', () => {
|
|
const root = fixture({ 'index.js': `module.exports = 1` }, { server: ['index.js'] }, {
|
|
name: 'x',
|
|
dependencies: { ws: '^8.21.0' },
|
|
})
|
|
try {
|
|
assert.deepStrictEqual(checkBundle(root).dependencies, ['ws'])
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('devDependencies are not runtime dependencies', () => {
|
|
// express, express-validator and swagger-autogen are all here and none of them
|
|
// ships: the shipped half is handed express on `ctx` (§2.3). A check that
|
|
// confused the two would fail on a correct repo, which is the one way to make
|
|
// everyone stop reading it.
|
|
const root = fixture({ 'index.js': `module.exports = 1` }, { server: ['index.js'] }, {
|
|
name: 'x',
|
|
devDependencies: { express: '^4.19.2' },
|
|
})
|
|
try {
|
|
assert.deepStrictEqual(runtimeDependencies(root), [])
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
// ── It has to reach requires that are not at the top level ─────────────────
|
|
|
|
test('follows requires written inside a function', () => {
|
|
// index.js requires inside `register()` because require order is load-bearing:
|
|
// `core.init(ctx)` has to run before anything under router/ is required. A
|
|
// check that only saw file-scope requires would miss every router this module
|
|
// has.
|
|
const root = fixture(
|
|
{
|
|
'index.js': `module.exports = function register(ctx) { const r = require('./router/a') }`,
|
|
'router/a.js': `module.exports = {}`,
|
|
},
|
|
{ server: ['index.js', 'router'] },
|
|
)
|
|
try {
|
|
assert.strictEqual(checkDeclaration(root).uncovered.size, 0)
|
|
assert.strictEqual(checkBundle(root).missing.length, 0)
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('follows requires transitively, not just one hop', () => {
|
|
const root = fixture(
|
|
{
|
|
'index.js': `require('./a')`,
|
|
'a.js': `require('./b')`,
|
|
'b.js': `require('./deep/c')`,
|
|
'deep/c.js': `module.exports = {}`,
|
|
},
|
|
{ server: ['index.js', 'a.js', 'b.js'] }, // `deep` missing
|
|
)
|
|
try {
|
|
assert.ok(checkDeclaration(root).uncovered.has('server/deep'))
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
// ── Resolution has to match Node's, or it invents failures ─────────────────
|
|
|
|
test('resolves a directory to its index.js', () => {
|
|
const root = fixture(
|
|
{ 'index.js': `require('./boot')`, 'boot/index.js': `module.exports = {}` },
|
|
{ server: ['index.js', 'boot'] },
|
|
)
|
|
try {
|
|
assert.strictEqual(checkDeclaration(root).uncovered.size, 0)
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('resolves a .json dependency, and does not try to parse it for requires', () => {
|
|
// server/index.js's last line requires ../module.json, which is why this case
|
|
// is not hypothetical and why `generated` is in the declared list at all.
|
|
const root = fixture(
|
|
{ 'index.js': `require('./data/atlas.json')`, 'data/atlas.json': `{"a":1}` },
|
|
{ server: ['index.js', 'data'] },
|
|
)
|
|
try {
|
|
const { uncovered, missing } = checkDeclaration(root)
|
|
assert.strictEqual(missing.length, 0)
|
|
assert.strictEqual(uncovered.size, 0)
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('survives a require cycle', () => {
|
|
const root = fixture(
|
|
{ 'index.js': `require('./a')`, 'a.js': `require('./index')` },
|
|
{ server: ['index.js', 'a.js'] },
|
|
)
|
|
try {
|
|
assert.strictEqual(checkDeclaration(root).uncovered.size, 0)
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('a specifier that resolves to nothing is reported, not thrown', () => {
|
|
const root = fixture({ 'index.js': `require('./gone')` })
|
|
try {
|
|
const { missing } = checkDeclaration(root)
|
|
assert.strictEqual(missing.length, 1)
|
|
assert.strictEqual(missing[0].specifier, './gone')
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test('prose describing a require is not a require', () => {
|
|
// The failure mode checkImports.js hit the first time it ran: index.js's own
|
|
// header explains why it must never require express, and comments in this repo
|
|
// name module paths constantly.
|
|
const root = fixture(
|
|
{ 'index.js': `// this file used to require('./router/gone')\nmodule.exports = 1` },
|
|
{ server: ['index.js'] },
|
|
)
|
|
try {
|
|
assert.strictEqual(checkDeclaration(root).missing.length, 0)
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
test("node_modules inside a bundle is npm's business, not this check's", () => {
|
|
// Nothing ships one today. The skip stays so that the day a dependency does
|
|
// arrive, this is not also the thing that breaks.
|
|
const root = fixture({
|
|
'index.js': `module.exports = 1`,
|
|
'node_modules/ws/index.js': `require('./lib/that-npm-owns')`,
|
|
})
|
|
try {
|
|
assert.strictEqual(checkBundle(root).missing.length, 0)
|
|
} finally {
|
|
cleanup(root)
|
|
}
|
|
})
|
|
|
|
// ── And the real repo, which is the check that actually gates a release ────
|
|
|
|
test('the real ci/bundle.json covers everything the real entry point reaches', () => {
|
|
const { uncovered, missing, reached, dependencies } = checkDeclaration()
|
|
assert.deepStrictEqual([...uncovered.keys()], [])
|
|
assert.deepStrictEqual(missing, [])
|
|
assert.deepStrictEqual(dependencies, [])
|
|
assert.ok(reached > 1, 'the walk should reach more than the entry point itself')
|
|
})
|
|
|
|
test('every path ci/bundle.json declares exists', () => {
|
|
// A list naming a path that has moved packs nothing and says nothing — `cp` in
|
|
// the release would fail, but only after the tag had been pushed.
|
|
for (const p of declaredServerPaths()) {
|
|
assert.ok(fs.existsSync(p), `ci/bundle.json names ${p}, which does not exist`)
|
|
}
|
|
})
|
|
|
|
test('the entry point is reachable from the declared list', () => {
|
|
const entry = path.resolve(__dirname, '..', 'index.js')
|
|
assert.ok(reachable(entry).files.includes(entry))
|
|
assert.ok(resolveFile(path.dirname(entry), './core'))
|
|
})
|