// ── A module's client chunk, served by the real app ──────────────────────── // // Phase 2, PR 7 of docs/website/MODULE_SYSTEM.md §2.7; the contract is // MODULE_API.md §3.1. moduleLoader.test.js proves the loader resolves and // validates the chunk; this file proves what the app does with the answer, and // it boots the REAL app.js to do it — because the three properties worth locking // are properties of the mount, not of the loader: // // 1. the module's own dist directory is published, and nothing above it; // 2. the chunk is served behind the module's state guard, so a failed or // disabled module's client half is as absent as its API; // 3. a miss is a 404 and never the SPA shell, which a browser would reject on // its MIME type after the request appeared to succeed. // // The modules directory is written and MODULES_DIR is set BEFORE app.js is // required, because the scan is synchronous and happens during that require. // Node's test runner gives each file its own process, so this cannot disturb // another test's view of the loader. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const fs = require('fs') const os = require('os') const path = require('path') const { test, before, after } = require('node:test') const assert = require('node:assert/strict') const CHUNK = 'export const hello = 1\n' const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rg-module-chunk-')) const dist = path.join(tmpRoot, 'uo', 'client', 'dist') fs.mkdirSync(dist, { recursive: true }) fs.writeFileSync(path.join(dist, 'entry.js'), CHUNK) fs.writeFileSync(path.join(dist, 'sidecar.js'), 'export const also = 2\n') // The two files a static mount rooted one level too high would publish. fs.writeFileSync(path.join(tmpRoot, 'uo', 'secrets.js'), 'const TOKEN = "leak"\n') fs.writeFileSync( path.join(tmpRoot, 'uo', 'module.json'), JSON.stringify({ id: 'uo', name: 'Ultima Online', version: '1.0.0', coreApi: '^1.0.0', client: { entry: 'client/dist/entry.js' }, }), ) process.env.MODULES_DIR = tmpRoot /* eslint-disable global-require */ const app = require('../src/app') const loader = require('../src/modules/loader') const db = require('../src/utils/db') const htmlShell = require('../src/utils/htmlShell') const settings = require('../src/model/settings/settings.model') /* eslint-enable global-require */ let server let base before(async () => { server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)) }) base = `http://127.0.0.1:${server.address().port}` // The state the module would be in after a clean boot. lifecycle.js does this // against the database; here it is set directly, since what is under test is // what the mount does with a state, not how the state was reached. loader.setState('uo', 'started') }) after(async () => { server.closeAllConnections() await new Promise((resolve) => server.close(resolve)) await db.close() fs.rmSync(tmpRoot, { recursive: true, force: true }) }) test('the chunk is served at the URL the shell injects', async () => { const [entryUrl] = loader.clientEntryUrls() assert.equal(entryUrl, '/modules/uo/entry.js') const res = await fetch(base + entryUrl) assert.equal(res.status, 200) assert.equal(await res.text(), CHUNK) // A module chunk is JavaScript to the browser or it is nothing: a `