refactor: separate standalone Vite wiring and provider middleware

This commit is contained in:
Sameh Khamis committed 2026-09-11 18:08:01 -07:00
1 parent 493acfc6aa
commit 8de1018c21
35 files changed
+8078 -7919

No files matched your search

+7
View File
@@ -13,6 +13,13 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
## [Unreleased]
### Changed
- Separate explicit browser build settings from standalone environment loading
and local provider middleware. Preserve provider behavior and root named exports.
- Rename standalone browser startup to `src/standalone/` and add a Node-only
`gods-eye-view/build/vite` export with checked package ownership.
### Development
- Extract application lifecycle and viewer exports. Split standalone startup into
+2 -2
View File
@@ -33,7 +33,7 @@ The highest-leverage places to jump in:
- **🌆 Add a CCTV source pack.** Austin is the reference camera source. Adding another city means a clean public camera catalog with coordinates, attribution, and server-registered frame URLs (the proxy only fetches registered URLs — never client-supplied ones, see [SECURITY.md](SECURITY.md)). City packs are the best first lane.
- **🛰️ Add or improve a data layer.** Each layer is one self-contained module in `src/data/<layer>.js` implementing the layer interface (`init/enable/disable/update/destroy/getStats`, optional `getDetectableObjects`/`getStats`). Use an existing layer as a template.
- **🎙️ Extend voice control.** Voice tools are declared server-side (`GEV_REALTIME_TOOLS` in `vite.config.js`) and executed client-side (`src/voice/gevActions.js`). Keep the tool surface tight and the responses honest (confirm only what actually happened).
- **🎙️ Extend voice control.** Voice tools are declared server-side (`GEV_REALTIME_TOOLS` in `server/providers/local.js`) and executed client-side (`src/voice/gevActions.js`). Keep the tool surface tight and the responses honest (confirm only what actually happened).
- **🎨 Add a visual style.** Styles are GLSL post-process shaders in `src/styles/`.
- **🐛 Fix bugs / improve the first-run experience.** See [docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md).
@@ -41,7 +41,7 @@ The highest-leverage places to jump in:
- **No framework.** Vanilla JS + [CesiumJS](https://cesium.com/platform/cesiumjs/) + [Vite](https://vitejs.dev/).
- **UI lives in `src/ui.js`** (panels, HUD, styles, the control facade). **Layer logic lives in `src/data/<layer>.js`.** Keep them separate.
- **Secrets stay server-side.** Anything needing a private key goes through a Vite proxy in `vite.config.js`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
- **Secrets stay server-side.** Anything needing a private key goes through a local proxy in `server/providers/local.js`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens.
- `docs/CURRENT-STATE.md` is the authoritative runtime reference — read it first.
## Coding style
+3 -3
View File
@@ -13,7 +13,7 @@ Include repro steps and impact. We'll acknowledge, investigate, and credit you (
## How secrets are handled
The golden rule: **secret-bearing API keys stay on the server side.** The dev/preview server (Vite middleware in `vite.config.js`) brokers every request that needs a private credential, so the browser never receives one.
The golden rule: **secret-bearing API keys stay on the server side.** The dev/preview server (middleware in `server/providers/local.js`) brokers every request that needs a private credential, so the browser never receives one.
| Key | Where it lives | How the browser uses it |
|-----|----------------|--------------------------|
@@ -29,7 +29,7 @@ These are designed to be used directly in the browser (like a Mapbox public toke
1. **Google Maps API key** — loads Photorealistic 3D Tiles directly and powers GEV place search. **Restrict it** (HTTP referrer + API restriction to the required Google APIs) in the Google Cloud Console. An unrestricted key in a public deployment can be abused and billed to you.
2. **Cesium ion token** (`CESIUM_ION_TOKEN`, optional — for ion-hosted Google Photorealistic 3D Tiles, Bing world imagery, and world terrain) — used as `Cesium.Ion.defaultAccessToken` client-side. Use a public **`assets:read`** token with **URL restrictions** for any hosted deployment. The Community plan has eligibility and usage limits; a public token is not a secret, but it can still consume the account's quota.
> The Vite `define` block in `vite.config.js` controls exactly what reaches the client: only these two keys. Everything else stays server-side.
> The explicit browser `define` block in `build/vite.js` controls exactly what reaches the client: only these two keys. Everything else stays server-side.
**Places and Street View never needed to be on that list** (#33): they're called from the server-side proxies in the table above, which use `GOOGLE_MAPS_SERVER_API_KEY` when it's set. Splitting it from the browser-exposed key lets each key's Google Cloud restriction actually match what it does — the browser key referrer-restricted to the APIs the client loads, the server key IP-restricted (never a referrer, since it never leaves your server) to Places + Street View Static — instead of one key that has to be either over-permissioned or broken for one of its two jobs. A single shared `GOOGLE_MAPS_API_KEY` still works if you don't split them; it just has to cover every API both sides use.
@@ -58,7 +58,7 @@ browser-key fallback for existing single-key setups.
## Server-side proxy hardening
The data proxies in `vite.config.js` are written so the browser cannot turn the server into an open relay:
The data proxies in `server/providers/local.js` are written so the browser cannot turn the server into an open relay:
- **No arbitrary-URL fetching.** The CCTV frame proxy fetches only server-registered camera/frame URLs — clients cannot pass an upstream URL to fetch (SSRF mitigation). Other proxies target fixed upstream hosts.
- **Radio is not an audio relay.** `/api/radio/stations` contacts only allowlisted Radio Browser HTTPS hosts and paths, rejects redirects, rejects any hostname with a loopback/private/link-local/metadata/non-public A or AAAA result, and pins each TLS connection to a validated address. It returns normalized public HTTPS stream URLs; `/api/radio/click/:uuid` applies the same destination policy and accepts only station IDs from the current bounded catalog. The browser then connects directly to the broadcaster after an explicit playback action, so the broadcaster sees the listener's IP address. GEV never proxies, caches, records, or redistributes audio.
+27
View File
@@ -0,0 +1,27 @@
import cesium from 'vite-plugin-cesium';
/** Build browser assets with explicit inputs; never load environment or providers. */
export function createBrowserViteConfig({
plugins = [], googleApiKey, cesiumToken, host = 'localhost', port = 4173,
} = {}) {
return {
plugins: [cesium(), ...plugins],
server: {
host: host || 'localhost',
port: parseInt(port, 10) || 4173,
allowedHosts: host === '0.0.0.0' || host === '::'
? true : ['localhost', '127.0.0.1', '.local'],
fs: { deny: ['.env', '.env.*', '*.{crt,pem}', '**/.git/**', '**/ENVIRONMENT'] },
// These headers protect the document containing Provider Settings.
headers: {
'X-Frame-Options': 'DENY',
'Content-Security-Policy': "frame-ancestors 'none'",
},
},
define: {
'import.meta.env.GOOGLE_MAPS_API_KEY': JSON.stringify(googleApiKey),
'import.meta.env.CESIUM_ION_TOKEN': JSON.stringify(cesiumToken),
},
build: { chunkSizeWarningLimit: 1500 },
};
}
+1 -1
View File
@@ -84,7 +84,7 @@ standalone share-restoration result keep their existing separate contracts.
## Standalone wiring
`src/main.js` reads the existing browser configuration and starts
`src/editions/local/application.js`. That module selects the four implementations
`src/standalone/application.js`. That module selects the four implementations
in its directory. Scene setup, controls, layer registration, tools and loading
chrome have separate owners. The existing `window.__godsEyeView` debugging shape
is preserved while the app is running.
+35 -3
View File
@@ -22,9 +22,9 @@ excluded. The formatter validates every entry before writing any file.
| `gods-eye-view/infrastructure/geojson` | Data loading, Cesium entities, selection handling and resource cleanup | A viewer and those same operations |
| `gods-eye-view/infrastructure/lod` | Pure visibility budgets and selection policy | Position/visibility records and camera measurements |
| `src/data/localGeojson.js` | Standalone compatibility wiring | The application's existing shared services |
| `src/main.js`, `src/editions/local/` and `vite.config.js` | Standalone startup and local Node services | Local configuration |
| `src/main.js` and `src/standalone/` | Standalone browser startup | Local configuration |
The scoped package exports are browser source modules. Use their documented
The application and infrastructure exports are browser source modules. Use their documented
exports instead of importing standalone startup or reaching into internal files.
The application owns the viewer, context store, overlay host and render scheduler;
layers use the supplied callbacks. See [the infrastructure contract](INFRASTRUCTURE-LAYERS.md).
@@ -42,7 +42,7 @@ They check build-time imports, not arbitrary runtime-generated module URLs.
Keep runtime module discovery out of these exports. When extracting another
component, add its ownership and consumer tests together. Node services must use
separate entry points and their own checks when they become reusable; importing
them into a browser component is not supported. No Node service is exported yet.
them into a browser component is not supported. No provider service is exported yet.
`gods-eye-view/application` owns construction order, startup state, cancellation
and disposal of caller-supplied components. Its only owned module is
@@ -53,3 +53,35 @@ Neither export imports standalone UI, layers, tools or configuration. See
UI panels and individual source adapters remain future extractions. They should
become smaller modules with explicit lifecycle owners as their callers migrate.
## Build and standalone server configuration
`gods-eye-view/build/vite` is a separate Node-only export. `build/vite.js`
creates standard Cesium/Vite browser settings from explicit inputs. It imports
only the declared `vite-plugin-cesium` build dependency, discovers no environment,
and constructs no provider middleware. Call it from a Vite configuration:
```js
import { createBrowserViteConfig } from 'gods-eye-view/build/vite';
export default createBrowserViteConfig({
plugins: [],
googleApiKey: undefined,
cesiumToken: undefined,
});
```
Consumers supply compatible Vite and vite-plugin-cesium development dependencies.
The package's `node` export condition has no browser fallback. The boundary gate
builds this group for Node, with the declared build dependency external; its
owned module list is checked just like browser groups. Browser groups cannot
use build-only dependency exceptions.
`server/standalone/vite.config.js` loads the root environment and passes selected
browser keys, host/port and the ordered local provider plugins to this helper.
`server/providers/local.js` still owns provider process state, routes and
credential-store paths. Provider Settings writes to the same root `.env` or
Pinokio store as before. `vite.config.js` preserves the default configuration and
existing named provider exports for tools/tests. The provider module remains
large; later extractions should split complete provider families and their tests.
+20 -2
View File
@@ -1,5 +1,23 @@
# God's Eye View Current State
## Build configuration and local provider boundaries
`vite.config.js` delegates to `server/standalone/vite.config.js`, which loads
this checkout's environment and constructs the local providers in their existing
order. `server/providers/local.js` holds the existing middleware and process
state; its named exports remain available through the root compatibility entry.
Provider URLs, key selection, cache behavior, setup restrictions and routes are
unchanged. Individual provider families remain to be split into smaller modules.
`gods-eye-view/build/vite` is a Node-only export for explicit browser build
settings: Cesium assets, caller-supplied plugins, browser key defines, server
binding and document/credential protections. It never reads an environment file
or constructs providers. The standalone caller owns those choices.
Browser startup now lives in `src/standalone/`. That directory contains only
browser code; Node configuration remains under `server/`. Application lifecycle
and viewer export paths are unchanged.
## Application startup and shutdown
The standalone entry now composes scene setup, controls, layer registration and
@@ -17,8 +35,8 @@ helpers do not import the standalone entry or discover configuration. See
## Scoped formatting and package checks
`npm run format` and `npm run format:check` operate on the explicit adopted-file
list. `npm run check:boundaries` checks the browser dependency graph of all
current package exports; infrastructure owns its three implementation modules
list. `npm run check:boundaries` checks the dependency graph of all
current package exports in their declared browser or Node runtime; infrastructure owns its three implementation modules
and takes Cesium from the consumer. CI runs both checks on Linux and Windows.
The standalone app, layer behavior and public export paths remain unchanged.
See [component ownership and adoption](CODE-BOUNDARIES.md).
+4 -1
View File
@@ -66,6 +66,9 @@
"./infrastructure/geojson": "./src/data/localGeojsonCore.js",
"./infrastructure/lod": "./src/data/localGeojsonLod.js",
"./application": "./src/app/application.js",
"./application/viewer": "./src/app/viewer.js"
"./application/viewer": "./src/app/viewer.js",
"./build/vite": {
"node": "./build/vite.js"
}
}
}
+15 -4
View File
@@ -3,7 +3,7 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { build, normalizePath } from 'vite';
/** Build each browser export group and reject imports outside its declared ownership. */
/** Build each browser or Node export group and reject imports outside its declared ownership. */
export async function checkPackageBoundaries(root) {
root = await realpath(root);
const readJson = async (name) =>
@@ -24,6 +24,10 @@ export async function checkPackageBoundaries(root) {
}
const reports = [];
for (const [name, group] of Object.entries(groups)) {
const node = group.runtime === 'node';
if (group.runtime && !['browser', 'node'].includes(group.runtime)) {
throw new Error(`Invalid boundary runtime: ${name}`);
}
if (
!Array.isArray(group.modules) ||
!group.modules.length ||
@@ -55,15 +59,20 @@ export async function checkPackageBoundaries(root) {
for (const external of group.external) {
if (
!Object.hasOwn(pkg.dependencies || {}, external) &&
!Object.hasOwn(pkg.peerDependencies || {}, external)
!Object.hasOwn(pkg.peerDependencies || {}, external) &&
!(node && Object.hasOwn(pkg.devDependencies || {}, external))
) {
throw new Error(
`Boundary external must be a declared runtime dependency: ${external}`,
`Boundary external must be a declared dependency for its runtime: ${external}`,
);
}
}
const input = group.exports.map((key) => {
const target = pkg.exports[key];
const declaration = pkg.exports[key];
const target = node ? declaration?.node : declaration;
if (node && (typeof declaration !== 'object' || Object.keys(declaration).join() !== 'node')) {
throw new Error(`Node export must have only a node condition: ${key}`);
}
if (
typeof target !== 'string' ||
!target.startsWith('./') ||
@@ -79,6 +88,7 @@ export async function checkPackageBoundaries(root) {
configFile: false,
envFile: false,
publicDir: false,
ssr: node ? { noExternal: true } : undefined,
logLevel: 'silent',
plugins: [
{
@@ -94,6 +104,7 @@ export async function checkPackageBoundaries(root) {
},
],
build: {
ssr: node,
lib: { entry: input, formats: ['es'] },
write: false,
minify: false,
+2 -2
View File
@@ -226,8 +226,8 @@ if [[ ! -f "src/data/cctv.js" ]]; then
exit 1
fi
if ! grep -q "dataManager.register(cctvLayer)" src/editions/local/data.js; then
echo "error: CCTV layer not wired in src/editions/local/data.js"
if ! grep -q "dataManager.register(cctvLayer)" src/standalone/data.js; then
echo "error: CCTV layer not wired in src/standalone/data.js"
exit 1
fi
+8 -8
View File
@@ -20,12 +20,12 @@
"src/app/mapLifecycle.test.mjs",
"src/app/startupCamera.test.mjs",
"src/app/viewer.js",
"src/editions/local/application.js",
"src/editions/local/controls.js",
"src/editions/local/data.js",
"src/editions/local/errors.js",
"src/editions/local/scene.js",
"src/editions/local/startupChrome.js",
"src/editions/local/startupChrome.test.mjs",
"src/editions/local/tools.js"
"src/standalone/application.js",
"src/standalone/controls.js",
"src/standalone/data.js",
"src/standalone/errors.js",
"src/standalone/scene.js",
"src/standalone/startupChrome.js",
"src/standalone/startupChrome.test.mjs",
"src/standalone/tools.js"
]
+30 -6
View File
@@ -10,16 +10,40 @@
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js"
],
"external": ["cesium"]
"external": [
"cesium"
]
},
"application": {
"exports": ["./application"],
"modules": ["src/app/application.js"],
"exports": [
"./application"
],
"modules": [
"src/app/application.js"
],
"external": []
},
"viewer": {
"exports": ["./application/viewer"],
"modules": ["src/app/viewer.js"],
"external": ["cesium"]
"exports": [
"./application/viewer"
],
"modules": [
"src/app/viewer.js"
],
"external": [
"cesium"
]
},
"vite-build": {
"runtime": "node",
"exports": [
"./build/vite"
],
"modules": [
"build/vite.js"
],
"external": [
"vite-plugin-cesium"
]
}
}
+2 -2
View File
@@ -13,7 +13,7 @@
*
* Every touched file is restored on exit, including on failure.
*
* NOTE: two mutations edit vite.config.js, and a running dev server watches that
* NOTE: two mutations edit server/providers/local.js, and a running dev server watches that
* file and restarts on every write. Writes are therefore content-guarded below
* so the file is touched exactly twice per mutation instead of on every
* iteration — enough that a dev server survives, but expect it to restart. If
@@ -31,7 +31,7 @@ const FILES = {
module: path.join(ROOT, 'src', 'firstRunExperience.js'),
html: path.join(ROOT, 'index.html'),
css: path.join(ROOT, 'style.css'),
vite: path.join(ROOT, 'vite.config.js'),
vite: path.join(ROOT, 'server/providers/local.js'),
main: path.join(ROOT, 'src', 'main.js'),
ui: path.join(ROOT, 'src', 'ui.js'),
docs: path.join(ROOT, 'docs', 'CURRENT-STATE.md'),
File diff suppressed because it is too large. Load diff
+21
View File
@@ -0,0 +1,21 @@
import { fileURLToPath } from 'node:url';
import { defineConfig, loadEnv } from 'vite';
import { createBrowserViteConfig } from '../../build/vite.js';
import { localProviderPlugins } from '../providers/local.js';
const root = fileURLToPath(new URL('../../', import.meta.url));
/** Load this checkout's configuration and attach its local provider middleware. */
export default defineConfig(({ mode }) => {
const loaded = loadEnv(mode, root, '');
for (const [key, value] of Object.entries(loaded)) {
if (process.env[key] === undefined) process.env[key] = value;
}
return createBrowserViteConfig({
plugins: localProviderPlugins(),
googleApiKey: process.env.GOOGLE_MAPS_API_KEY,
cesiumToken: process.env.CESIUM_ION_TOKEN,
host: process.env.HOST,
port: process.env.PORT,
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ import fs from 'node:fs';
import test from 'node:test';
import { filterTrailing24h, parseFirmsCsv } from './firmsCsv.js';
const config = fs.readFileSync(new URL('../../vite.config.js', import.meta.url), 'utf8');
const config = fs.readFileSync(new URL('../../server/providers/local.js', import.meta.url), 'utf8');
const start = config.indexOf(' async function refreshUpstream(key) {');
assert.notEqual(start, -1, 'FIRMS refresh function exists');
const end = config.indexOf('\n }', start);
+2 -2
View File
@@ -20,8 +20,8 @@ async function launch(overrides = {}, dotenv = '') {
for (const name of ['dev-cctv.sh', 'dev-fresh.sh', 'read-dotenv-value.mjs']) {
await fs.copyFile(new URL(`../scripts/${name}`, import.meta.url), path.join(root, 'scripts', name));
}
await fs.mkdir(path.join(root, 'src', 'editions', 'local'), { recursive: true });
await fs.copyFile(new URL('./editions/local/data.js', import.meta.url), path.join(root, 'src', 'editions', 'local', 'data.js'));
await fs.mkdir(path.join(root, 'src', 'standalone'), { recursive: true });
await fs.copyFile(new URL('./standalone/data.js', import.meta.url), path.join(root, 'src', 'standalone', 'data.js'));
await fs.mkdir(path.join(root, 'node_modules'));
await fs.symlink(fileURLToPath(new URL('../node_modules/vite', import.meta.url)), path.join(root, 'node_modules', 'vite'), 'dir');
await fs.writeFile(path.join(root, '.env'), dotenv);
+3 -3
View File
@@ -546,7 +546,7 @@ test('the decision table is written down where the next editor will read it', ()
test('markup, startup ordering and accessibility remain pinned', () => {
const html = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const startup = fs.readFileSync(new URL('./editions/local/startupChrome.js', import.meta.url), 'utf8');
const startup = fs.readFileSync(new URL('./standalone/startupChrome.js', import.meta.url), 'utf8');
const css = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
assert.match(html, /id="first-run-launcher" role="dialog"[^>]*aria-labelledby="first-run-title"[^>]*hidden/);
@@ -648,7 +648,7 @@ test('the DISPLAY rail starts collapsed on a first run, and a stored choice wins
// ── Voice: instruction-only, tool schema byte-unchanged ─────────────────────
test('the voice TOOL SCHEMA is byte-identical to main — the mission mapping is instructions only', () => {
const src = fs.readFileSync(new URL('../vite.config.js', import.meta.url), 'utf8');
const src = fs.readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
const start = src.indexOf('const GEV_REALTIME_TOOLS = [');
assert.ok(start > 0, 'GEV_REALTIME_TOOLS must still be a single literal array');
const end = src.indexOf('\n];\n', start);
@@ -687,7 +687,7 @@ test('the voice TOOL SCHEMA is byte-identical to main — the mission mapping is
});
test('every layer a mission drives is already in the shipped set_layer_visibility enum', () => {
const src = fs.readFileSync(new URL('../vite.config.js', import.meta.url), 'utf8');
const src = fs.readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
const tool = src.slice(src.indexOf("name: 'set_layer_visibility'"), src.indexOf("name: 'show_data_layers_menu'"));
const missionLayerIds = Object.values(FIRST_RUN_MISSIONS).flatMap((mission) => mission.layerIds || []);
assert.ok(missionLayerIds.length > 0);
+1 -1
View File
@@ -28,7 +28,7 @@ test('the boot provenance snapshot survives in-process Vite config re-evaluation
// panel save has already set its values live on process.env. A recomputed
// snapshot would classify the panel's own keys as external (read-only) until
// a full process relaunch, so the first evaluation's snapshot must win.
const source = readFileSync(new URL('../vite.config.js', import.meta.url), 'utf8');
const source = readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
assert.match(
source,
/const PROVIDER_ENV_AT_BOOT = globalThis\.__GEV_PROVIDER_ENV_AT_BOOT \?\?= Object\.freeze\(/,
+3 -3
View File
@@ -1,7 +1,7 @@
import { createLocalApplication } from './editions/local/application.js';
import { describeError } from './editions/local/errors.js';
import { createStandaloneApplication } from './standalone/application.js';
import { describeError } from './standalone/errors.js';
const application = createLocalApplication({
const application = createStandaloneApplication({
googleApiKey: import.meta.env.GOOGLE_MAPS_API_KEY,
cesiumToken: import.meta.env.CESIUM_ION_TOKEN,
allowQaRegistration: import.meta.env.DEV,
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
const source = readFileSync(new URL('../vite.config.js', import.meta.url), 'utf8');
const source = readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
const detail = 'fixture-secret-token /internal/example <html>';
// Execute the production middleware with isolated upstreams and cache storage.
+1 -1
View File
@@ -8,7 +8,7 @@ const ui = readFileSync(new URL('./ui.js', import.meta.url), 'utf8');
const radio = readFileSync(new URL('./data/radio.js', import.meta.url), 'utf8');
const rocketLaunches = readFileSync(new URL('./data/rocketLaunches.js', import.meta.url), 'utf8');
const realtime = readFileSync(new URL('./voice/gevRealtime.js', import.meta.url), 'utf8');
const voice = readFileSync(new URL('../vite.config.js', import.meta.url), 'utf8');
const voice = readFileSync(new URL('../server/providers/local.js', import.meta.url), 'utf8');
const css = readFileSync(new URL('../style.css', import.meta.url), 'utf8');
/** Parse the Realtime tool array out of the Vite config as real data. */
@@ -1,14 +1,14 @@
import { createApplication } from '../../app/application.js';
import { createLocalScene } from './scene.js';
import { createLocalControls } from './controls.js';
import { createLocalData } from './data.js';
import { createLocalTools } from './tools.js';
import { createApplication } from '../app/application.js';
import { createStandaloneScene } from './scene.js';
import { createStandaloneControls } from './controls.js';
import { createStandaloneData } from './data.js';
import { createStandaloneTools } from './tools.js';
// The existing controls and layer catalog contain page-scoped state.
let constructed = false;
/** Compose the standalone application once per page. Reload to start again. */
export function createLocalApplication({
export function createStandaloneApplication({
googleApiKey,
cesiumToken,
allowQaRegistration = false,
@@ -20,11 +20,11 @@ export function createLocalApplication({
const loaderStatus = loadingScreen.querySelector('.loader-status');
return createApplication({
createScene: (context) =>
createLocalScene({ ...context, googleApiKey, cesiumToken, loaderStatus }),
createStandaloneScene({ ...context, googleApiKey, cesiumToken, loaderStatus }),
createControls: (context) =>
createLocalControls({ ...context, loaderStatus }),
createStandaloneControls({ ...context, loaderStatus }),
createData: (context) =>
createLocalData({ ...context, allowQaRegistration }),
createTools: (context) => createLocalTools({ ...context, loadingScreen }),
createStandaloneData({ ...context, allowQaRegistration }),
createTools: (context) => createStandaloneTools({ ...context, loadingScreen }),
});
}
@@ -1,9 +1,9 @@
import { StyleManager } from '../../ui.js';
import { flyToAustin } from '../../camera.js';
import { initCockpitCloudEffects } from '../../cockpitCloudEffects.js';
import { StyleManager } from '../ui.js';
import { flyToAustin } from '../camera.js';
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
/** Construct the existing controls and camera presentation. */
export function createLocalControls({
export function createStandaloneControls({
scene: { viewer, mapStackController },
loaderStatus,
defer,
@@ -1,21 +1,21 @@
import { DataLayerManager } from '../../data/manager.js';
import flightsLayer from '../../data/flights.js';
import militaryFlightsLayer from '../../data/militaryFlights.js';
import earthquakesLayer from '../../data/earthquakes.js';
import satellitesLayer from '../../data/satellites.js';
import rocketLaunchesLayer from '../../data/rocketLaunches.js';
import trafficLayer from '../../data/traffic.js';
import cctvLayer from '../../data/cctv.js';
import radioLayer from '../../data/radio.js';
import bikeshareLayer from '../../data/bikeshare.js';
import aisLiveVesselsLayer from '../../data/aisLiveVessels.js';
import militaryInstallationsLayer from '../../data/militaryInstallations.js';
import militaryAwarenessLayer from '../../data/militaryAwareness.js';
import localDataLayers from '../../data/localLayers.js';
import { LAYER_STATE_REGISTRY } from '../../data/layerState.js';
import { DataLayerManager } from '../data/manager.js';
import flightsLayer from '../data/flights.js';
import militaryFlightsLayer from '../data/militaryFlights.js';
import earthquakesLayer from '../data/earthquakes.js';
import satellitesLayer from '../data/satellites.js';
import rocketLaunchesLayer from '../data/rocketLaunches.js';
import trafficLayer from '../data/traffic.js';
import cctvLayer from '../data/cctv.js';
import radioLayer from '../data/radio.js';
import bikeshareLayer from '../data/bikeshare.js';
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
import militaryInstallationsLayer from '../data/militaryInstallations.js';
import militaryAwarenessLayer from '../data/militaryAwareness.js';
import localDataLayers from '../data/localLayers.js';
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
/** Register the standalone layer catalog before allowing state restoration. */
export function createLocalData({
export function createStandaloneData({
scene: { viewer },
controls: { styleManager },
allowQaRegistration,
File renamed without changes.
@@ -1,15 +1,15 @@
import * as Cesium from 'cesium';
import { createApplicationViewer } from '../../app/viewer.js';
import { registerDataCredits } from '../../data/dataCredits.js';
import { configureCreditKeyboardAccess } from '../../creditKeyboard.js';
import { MapStackController } from '../../mapStackController.js';
import { loadPhotorealisticTileset } from '../../mapStartup.js';
import { initLogoGaze } from '../../logoGaze.js';
import { uninstallRenderGovernor } from '../../renderGovernor.js';
import { createApplicationViewer } from '../app/viewer.js';
import { registerDataCredits } from '../data/dataCredits.js';
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
import { MapStackController } from '../mapStackController.js';
import { loadPhotorealisticTileset } from '../mapStartup.js';
import { initLogoGaze } from '../logoGaze.js';
import { uninstallRenderGovernor } from '../renderGovernor.js';
import { describeError } from './errors.js';
/** Construct the standalone globe using the caller's local configuration. */
export async function createLocalScene({
export async function createStandaloneScene({
googleApiKey,
cesiumToken,
loaderStatus,
@@ -1,8 +1,8 @@
import { initFirstRunExperience } from '../../firstRunExperience.js';
import { initKeySetup } from '../../keySetup.js';
import { initFirstRunExperience } from '../firstRunExperience.js';
import { initKeySetup } from '../keySetup.js';
/** Reveal welcome controls only after restoration and the loading transition. */
export function startLocalChrome({
export function startStandaloneChrome({
loadingScreen,
styleManager,
dataManager,
@@ -37,7 +37,7 @@ function fixture() {
};
vm.createContext(context);
vm.runInContext(source, context);
const stop = context.startLocalChrome({
const stop = context.startStandaloneChrome({
loadingScreen: {
classList: { add: (value) => events.push(value) },
addEventListener: (type, listener) => listeners.set(type, listener),
@@ -1,18 +1,18 @@
import { SceneDirector } from '../../scenes/director.js';
import { initAnnotations } from '../../annotations/index.js';
import { initGevVoiceCommands } from '../../voice/gevRealtime.js';
import { installScopeMask, destroyScopeMask } from '../../scopeMask.js';
import { SceneDirector } from '../scenes/director.js';
import { initAnnotations } from '../annotations/index.js';
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
import {
installRenderGovernor,
getRenderGovernorDiagnostics,
governorRequestRender,
holdContinuousRender,
releaseContinuousRender,
} from '../../renderGovernor.js';
import { startLocalChrome } from './startupChrome.js';
} from '../renderGovernor.js';
import { startStandaloneChrome } from './startupChrome.js';
/** Attach scene tools, rendering listeners and the standalone debug handle. */
export function createLocalTools({
export function createStandaloneTools({
scene,
controls,
data,
@@ -30,7 +30,7 @@ export function createLocalTools({
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
annotations.destroy();
});
defer(startLocalChrome({ loadingScreen, styleManager, dataManager, signal }));
defer(startStandaloneChrome({ loadingScreen, styleManager, dataManager, signal }));
// Idle render governor: flips the scene into requestRenderMode whenever
// nothing animates per frame. Installed AFTER every module above has had
// its chance to register pre-install holds. (perf wave 2)
+14
View File
@@ -75,3 +75,17 @@ test('dynamic imports obey the same component ownership rule', async (t) => {
await writeFile(path.join(root, 'startup.js'), 'export const app = 2;');
await assert.rejects(checkPackageBoundaries(root), /unowned module.*startup/);
});
test('a Node build export must be explicitly classified and scoped', async (t) => {
const root = await fixture(t);
await writeFile(path.join(root, 'package.json'), JSON.stringify({
name: 'boundary-fixture', type: 'module', exports: { './feature': { node: './entry.js' } },
}));
await writeFile(path.join(root, 'scripts/package-boundaries.json'), JSON.stringify({
feature: { runtime: 'node', exports: ['./feature'], modules: ['entry.js'], external: [] },
}));
assert.deepEqual(await checkPackageBoundaries(root), [{ name: 'feature', exports: 1, modules: 1 }]);
await writeFile(path.join(root, 'entry.js'), "import './startup.js'; export const value = 1;");
await writeFile(path.join(root, 'startup.js'), 'export const app = 2;');
await assert.rejects(checkPackageBoundaries(root), /unowned module.*startup/);
});
+52
View File
@@ -0,0 +1,52 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createBrowserViteConfig } from '../../build/vite.js';
import standaloneConfig, * as compatibility from '../../vite.config.js';
import * as providers from '../../server/providers/local.js';
test('explicit build inputs preserve browser-only defines, plugin order and loopback protections', () => {
const plugin = { name: 'fixture-provider' };
const config = createBrowserViteConfig({ plugins: [plugin], googleApiKey: 'browser-fixture', cesiumToken: 'ion-fixture' });
assert.equal(config.plugins[1], plugin);
assert.equal(config.server.host, 'localhost');
assert.equal(config.server.port, 4173);
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1', '.local']);
assert.ok(config.server.fs.deny.includes('**/ENVIRONMENT'));
assert.ok(config.server.fs.deny.includes('.env.*'));
assert.equal(config.server.headers['X-Frame-Options'], 'DENY');
assert.equal(config.server.headers['Content-Security-Policy'], "frame-ancestors 'none'");
assert.deepEqual(config.define, {
'import.meta.env.GOOGLE_MAPS_API_KEY': '"browser-fixture"',
'import.meta.env.CESIUM_ION_TOKEN': '"ion-fixture"',
});
assert.equal(createBrowserViteConfig({ host: '0.0.0.0', port: '4800' }).server.allowedHosts, true);
assert.equal(createBrowserViteConfig({ host: '::', port: '4800' }).server.port, 4800);
});
test('build helper does not discover environment values or construct local providers', () => {
const before = process.env.GOOGLE_MAPS_API_KEY;
process.env.GOOGLE_MAPS_API_KEY = 'environment-fixture';
try {
const config = createBrowserViteConfig();
assert.equal(config.define['import.meta.env.GOOGLE_MAPS_API_KEY'], undefined);
assert.equal(config.plugins.length, 1);
} finally {
if (before === undefined) delete process.env.GOOGLE_MAPS_API_KEY;
else process.env.GOOGLE_MAPS_API_KEY = before;
}
});
test('root config retains existing named exports and standalone provider order', () => {
for (const [name, value] of Object.entries(providers)) assert.equal(compatibility[name], value, name);
const config = standaloneConfig({ mode: 'test' });
assert.deepEqual(config.plugins.slice(1).map((plugin) => plugin.name), providers.localProviderPlugins().map((plugin) => plugin.name));
assert.equal(config.plugins.at(-1).name, 'gev-key-setup');
});
test('build export resolves in Node and has no browser fallback', async () => {
const exported = await import('gods-eye-view/build/vite');
assert.equal(exported.createBrowserViteConfig, createBrowserViteConfig);
const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url)));
assert.deepEqual(pkg.exports['./build/vite'], { node: './build/vite.js' });
});
+1 -1
View File
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const voiceConfig = readFileSync(new URL('../../vite.config.js', import.meta.url), 'utf8');
const voiceConfig = readFileSync(new URL('../../server/providers/local.js', import.meta.url), 'utf8');
const realtime = readFileSync(new URL('./gevRealtime.js', import.meta.url), 'utf8');
test('aircraft identity narration acknowledges missing enrichment', () => {
+1 -1
View File
@@ -35,7 +35,7 @@ test('every live basemap is reachable by its own id — no enum value without a
assert.equal(normalizeStackId('Esri'), 'esri-imagery');
assert.equal(normalizeStackId('esri imagery'), 'esri-imagery');
// And the voice tool's enum must equal the set of live ids — no drift either way.
const config = readFileSync(new URL('../../vite.config.js', import.meta.url), 'utf8');
const config = readFileSync(new URL('../../server/providers/local.js', import.meta.url), 'utf8');
const enumMatch = config.match(/enum: \[('photoreal'[^\]]*)\],\s*\n\s*description: 'photoreal = Google 3D/);
assert.ok(enumMatch, 'set_map_stack enum literal must still be findable');
const enumIds = enumMatch[1].split(',').map((s) => s.trim().replace(/^'|'$/g, ''));
+3 -7821
View File
File diff suppressed because it is too large. Load diff