// ============ linkcheck.mjs — import EVERY module under a DOM stub ============ // Catches missing/misnamed exports (module linking errors) without a browser. // Run: node tests/linkcheck.mjs let failures = 0; const makeCtx = () => new Proxy({}, { get(t, p) { if (p === 'canvas') return {}; if (!(p in t)) t[p] = (...args) => makeCtx(); const v = t[p]; return typeof v === 'function' ? v : t[p]; }, set() { return true; }, }); const fakeEl = (id = '') => { const el = { id, style: {}, dataset: {}, children: [], classList: { add() {}, remove() {}, toggle() {}, contains: () => true }, setAttribute() {}, getAttribute: () => null, addEventListener() {}, removeEventListener() {}, appendChild(c) { el.children.push(c); }, remove() {}, click() {}, querySelectorAll: () => [], querySelector: () => null, getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }), getContext: () => makeCtx(), focus() {}, select() {}, files: [], text: '', _innerHTML: '', firstChild: null, }; Object.defineProperty(el, 'innerHTML', { get() { return el._innerHTML; }, set(v) { el._innerHTML = String(v); if (!el._innerHTML) el.children = []; el.firstChild = el.children[0] ?? null; }, }); Object.defineProperty(el, 'textContent', { get() { return el._textContent ?? ''; }, set(v) { el._textContent = String(v); }, }); let width = 300, height = 150; Object.defineProperty(el, 'width', { get: () => width, set: v => { width = v; } }); Object.defineProperty(el, 'height', { get: () => height, set: v => { height = v; } }); return el; }; globalThis.window = globalThis; globalThis.innerWidth = 1280; globalThis.innerHeight = 800; globalThis.requestAnimationFrame = () => 0; globalThis.cancelAnimationFrame = () => {}; globalThis.addEventListener = () => {}; globalThis.removeEventListener = () => {}; globalThis.localStorage = { _m: {}, getItem(k) { return this._m[k] ?? null; }, setItem(k, v) { this._m[k] = String(v); }, removeItem(k) { delete this._m[k]; } }; globalThis.document = { getElementById: id => (globalThis.__els ??= {})[id] ?? ((globalThis.__els[id] = fakeEl(id))), createElement: tag => fakeEl(tag), querySelectorAll: () => [], querySelector: () => null, addEventListener() {}, body: { appendChild() {} }, }; globalThis.AudioContext = undefined; globalThis.Blob = class { constructor() {} }; globalThis.URL = { createObjectURL: () => 'blob:x', revokeObjectURL: () => {} }; async function load(label, path) { try { await import(path); console.log(`✔ ${label}`); } catch (e) { failures++; console.error(`✘ ${label}: ${e.message}`); console.error(e.stack?.split('\n').slice(1, 3).join('\n')); } } await load('core/util', '../js/core/util.js'); await load('core/config', '../js/core/config.js'); await load('core/audio', '../js/core/audio.js'); await load('world/map', '../js/world/map.js'); await load('world/path', '../js/world/path.js'); await load('game/economy', '../js/game/economy.js'); await load('game/state', '../js/game/state.js'); await load('game/guests', '../js/game/guests.js'); await load('game/staff', '../js/game/staff.js'); await load('game/rides', '../js/game/rides.js'); await load('game/coaster', '../js/game/coaster.js'); await load('game/heroes', '../js/game/heroes.js'); await load('game/magic', '../js/game/magic.js'); await load('game/research', '../js/game/research.js'); await load('game/save', '../js/game/save.js'); await load('render/renderer', '../js/render/renderer.js'); await load('ui/ui', '../js/ui/ui.js'); await load('ui/dialogs', '../js/ui/dialogs.js'); await load('ui/povui', '../js/ui/povui.js'); await load('main (full graph)', '../js/main.js'); console.log(failures ? `\n${failures} LINK FAILURES` : '\nALL MODULES LINK OK'); process.exit(failures ? 1 : 0);