mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Cesium's script sets CESIUM_WORKERS itself when it loads, so the panel's combined workers script was never used, and neither was the embedded terrain-heights file: the worker still requested it from the panel page's site. The panel build now writes only the prelude, and the panel puts it ahead of whatever Cesium assigns to CESIUM_WORKERS. The separate esbuild bundle of Cesium's workers is gone.
154 lines
5.1 KiB
JavaScript
154 lines
5.1 KiB
JavaScript
/**
|
|
* The MCP Apps panel build. A panel loads the app through its MCP server,
|
|
* one tool call per file (see src/tools/globePanel.js), so the app must be
|
|
* few files: one app script, one stylesheet, and Cesium's own script, which
|
|
* carries its workers and starts them from memory. The build is served at
|
|
* PANEL_BASE next to the app.
|
|
*/
|
|
|
|
// The panel loads these paths (src/tools/globePanel.js); a test keeps the
|
|
// two in step.
|
|
export const PANEL_BASE = '/panel/';
|
|
/** The script the panel runs ahead of Cesium's workers (workerFilesPrelude). */
|
|
export const PANEL_WORKER_PRELUDE_PATH = 'cesium/worker-prelude.js';
|
|
export const PANEL_OUT_DIR = 'dist/panel';
|
|
|
|
/** A browser Vite config changed to produce the panel build. */
|
|
export function panelBuildConfig(config) {
|
|
return {
|
|
...config,
|
|
base: PANEL_BASE,
|
|
build: {
|
|
...config.build,
|
|
outDir: PANEL_OUT_DIR,
|
|
emptyOutDir: true,
|
|
modulePreload: false,
|
|
cssCodeSplit: false,
|
|
rollupOptions: {
|
|
...config.build?.rollupOptions,
|
|
output: { inlineDynamicImports: true },
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
const CONTENT_TYPES = {
|
|
'.css': 'text/css',
|
|
'.geojsonl': 'application/geo+json-seq',
|
|
'.glb': 'model/gltf-binary',
|
|
'.html': 'text/html',
|
|
'.jpg': 'image/jpeg',
|
|
'.js': 'text/javascript',
|
|
'.json': 'application/json',
|
|
'.png': 'image/png',
|
|
'.svg': 'image/svg+xml',
|
|
'.wasm': 'application/wasm',
|
|
'.woff2': 'font/woff2',
|
|
'.xml': 'application/xml',
|
|
};
|
|
|
|
/**
|
|
* Vite plugin serving the panel build at PANEL_BASE on the dev server, as
|
|
* the preview server and production builds serve it from the build output.
|
|
*/
|
|
export function panelBuildPlugin({ outDir = PANEL_OUT_DIR } = {}) {
|
|
return {
|
|
name: 'panel-build',
|
|
configureServer(server) {
|
|
server.middlewares.use(async (req, res, next) => {
|
|
const { pathname } = new URL(req.url || '/', 'http://localhost');
|
|
if (!pathname.startsWith(PANEL_BASE)) return next();
|
|
const { readFile } = await import('node:fs/promises');
|
|
const { extname, resolve, sep } = await import('node:path');
|
|
const root = resolve(server.config.root, outDir);
|
|
let relative;
|
|
try {
|
|
relative = decodeURIComponent(pathname.slice(PANEL_BASE.length));
|
|
} catch {
|
|
res.statusCode = 400;
|
|
res.end('Bad path');
|
|
return;
|
|
}
|
|
const file = resolve(root, relative || 'index.html');
|
|
if (file !== root && !file.startsWith(root + sep)) return next();
|
|
try {
|
|
const body = await readFile(file);
|
|
res.setHeader(
|
|
'Content-Type',
|
|
CONTENT_TYPES[extname(file)] || 'application/octet-stream',
|
|
);
|
|
res.end(body);
|
|
} catch {
|
|
res.statusCode = 404;
|
|
res.end(
|
|
relative ? 'Not found' : 'No panel build; run npm run build:panel',
|
|
);
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cesium files its workers load themselves, as paths under Cesium's base.
|
|
* Workers request them from the panel page's own site, which has none of
|
|
* the app's files, so the panel build embeds them in the workers script.
|
|
*/
|
|
export const PANEL_WORKER_FILES = Object.freeze([
|
|
'Assets/approximateTerrainHeights.json',
|
|
]);
|
|
|
|
/**
|
|
* A script, run in each worker before Cesium's, that answers the worker's
|
|
* requests for `files` (Cesium-relative path → text) from memory and lets
|
|
* every other request through.
|
|
*/
|
|
export function workerFilesPrelude(files) {
|
|
const answer = (FILES) => {
|
|
const find = (url) => {
|
|
const path = String(url).split(/[?#]/)[0];
|
|
for (const name of Object.keys(FILES))
|
|
if (path.endsWith(`/${name}`)) return FILES[name];
|
|
return null;
|
|
};
|
|
const xhr = XMLHttpRequest.prototype;
|
|
const { open, send, setRequestHeader } = xhr;
|
|
xhr.open = function (method, url, ...rest) {
|
|
this.__gevFile = find(url);
|
|
if (this.__gevFile === null) return open.call(this, method, url, ...rest);
|
|
};
|
|
xhr.setRequestHeader = function (...args) {
|
|
if (this.__gevFile === null) return setRequestHeader.apply(this, args);
|
|
};
|
|
xhr.send = function (body) {
|
|
const text = this.__gevFile;
|
|
if (text === null || text === undefined) return send.call(this, body);
|
|
const type = this.responseType;
|
|
const response =
|
|
type === 'json'
|
|
? JSON.parse(text)
|
|
: type === 'arraybuffer'
|
|
? new TextEncoder().encode(text).buffer
|
|
: text;
|
|
const state = {
|
|
readyState: 4,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
response,
|
|
responseText: typeof response === 'string' ? response : '',
|
|
getAllResponseHeaders: () => 'content-type: application/json\r\n',
|
|
getResponseHeader: (name) =>
|
|
name.toLowerCase() === 'content-type' ? 'application/json' : null,
|
|
};
|
|
for (const [name, value] of Object.entries(state))
|
|
Object.defineProperty(this, name, { configurable: true, value });
|
|
setTimeout(() => {
|
|
this.dispatchEvent(new Event('readystatechange'));
|
|
this.dispatchEvent(new ProgressEvent('load'));
|
|
this.dispatchEvent(new ProgressEvent('loadend'));
|
|
});
|
|
};
|
|
};
|
|
return `(${answer})(${JSON.stringify(files)});\n`;
|
|
}
|