// `module.json` is what core validates before it will load anything, and every // rule it is checked against lives in core's loader (MODULE_API.md §2.1, §4.3). // Restating those rules here means a manifest mistake fails in this repo's CI, // which can say what is wrong, rather than on an install, where the symptom is a // module that is simply absent. // // These are the loader's own patterns, copied deliberately rather than imported: // this repo has no dependency on core's source, and a copy that drifts is // exactly what the coreApi range exists to catch. const test = require('node:test') const assert = require('node:assert') const fs = require('node:fs') const path = require('node:path') const ROOT = path.resolve(__dirname, '..', '..') const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'module.json'), 'utf8')) const KEYS = new Set([ 'id', 'name', 'version', 'coreApi', 'server', 'client', 'schema', 'purge', 'mounts', 'extensions', 'capabilities', ]) const ID = /^[a-z][a-z0-9-]{1,31}$/ const PREFIX = /^\/[a-z0-9][a-z0-9-]*$/ const TIERS = ['public', 'admin', 'player'] test('declares no key core would reject', () => { for (const key of Object.keys(manifest)) { assert.ok(KEYS.has(key), `unknown key "${key}" — core rejects rather than ignores it`) } }) test('id is "uo" and matches the directory it installs as', () => { assert.ok(ID.test(manifest.id)) assert.strictEqual(manifest.id, 'uo') }) test('version and coreApi are present and semver-shaped', () => { assert.match(manifest.version, /^\d+\.\d+\.\d+/) assert.match(manifest.coreApi, /^[\^~]?\d+\.\d+\.\d+/) }) test('every declared file exists', () => { for (const key of ['server', 'schema', 'purge']) { if (manifest[key]) { assert.ok(fs.existsSync(path.join(ROOT, manifest[key])), `${key}: ${manifest[key]} is missing`) } } }) test('a schema fragment always comes with a purge', () => { // Core enforces this too. A module that can create tables and cannot drop them // leaves an operator with orphaned data and no supported way to remove it. if (manifest.schema) assert.ok(manifest.purge, 'declares schema but no purge') }) test('client.entry is in a subdirectory, because its directory is what gets served', () => { assert.ok(manifest.client, 'module-uo ships a client half') const entry = manifest.client.entry assert.match(path.basename(entry), /^[A-Za-z0-9][A-Za-z0-9._-]*\.js$/) // The rule worth a test of its own: core serves `dirname(entry)`, so an entry // in the module root would publish the server source and module.json. assert.notStrictEqual(path.dirname(path.resolve(ROOT, entry)), ROOT) assert.ok(path.resolve(ROOT, entry).startsWith(ROOT + path.sep), 'entry escapes the module root') }) test('declared mounts are single lowercase segments in known tiers', () => { for (const [tier, prefixes] of Object.entries(manifest.mounts || {})) { assert.ok(TIERS.includes(tier), `unknown tier "${tier}"`) for (const prefix of prefixes) assert.match(prefix, PREFIX) } }) test('the manifest claims a coherent bundle', () => { // `capabilities` is published by GET /api/v1/public/modules and is what a // client feature-detects against — the SPA and the Android app both read it — // so it must not claim something the module does not serve. Checked as a shape // rather than a list: which capabilities exist is a product decision, that // they are non-empty opaque strings is the contract. for (const c of manifest.capabilities || []) { assert.match(c, /^[a-z][a-z0-9-]*$/, `capability "${c}" is not an opaque lowercase id`) } // Declaring a mount is what makes the prefix this module's; the loader checks // the declaration against what register() actually registers (entry.test.js). assert.ok(Object.keys(manifest.mounts).length > 0, 'a module that mounts nothing serves nothing') assert.deepStrictEqual(manifest.extensions, ['admin.users.detail']) })