mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor: separate standalone Vite wiring and provider middleware
This commit is contained in:
1 parent
493acfc6aa
commit
8de1018c21
35 files changed
+8078
-7919
No files matched your search
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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/);
|
||||
});
|
||||
@@ -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' });
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user