refactor(build): place the views in the page instead of expanding templates

index.html now lists each view's host and the loading screen itself, so
the gev:template expansion plugin, its view list, the loading-screen
template and the ./build/html export are gone. createApplication takes
omitViews for views an application leaves out; their hosts are removed,
and a view an earlier call rendered is unmounted.
This commit is contained in:
Sameh Khamis committed 2026-10-31 17:52:47 -07:00
1 parent f279200af5
commit 5bf797ef71
18 files changed
+176 -174

No files matched your search

+5 -2
View File
@@ -7,7 +7,9 @@ const STOP_ORDER = ['tools', 'controls', 'data', 'scene'];
* Own application startup and teardown without importing an application entry.
* Constructors receive earlier components, an AbortSignal and defer(cleanup).
* Register cleanup immediately after acquiring each resource, before any await.
* @param {{createScene:Function,createControls:Function,createData:Function,createTools:Function}} constructors
* The page's views render first; `omitViews` names the ones this application
* leaves out (see mountApplicationViews).
* @param {{createScene:Function,createControls:Function,createData:Function,createTools:Function,omitViews?:string[]}} constructors
* @returns {{start:Function,destroy:Function,subscribe:Function,getState:Function,getComponents:Function}}
*/
export function createApplication({
@@ -15,9 +17,10 @@ export function createApplication({
createControls,
createData,
createTools,
omitViews = [],
}) {
// A host's page needs its views before any phase reads the markup.
mountApplicationViews();
mountApplicationViews(globalThis.document, { omit: omitViews });
const factories = {
scene: createScene,
controls: createControls,
+5 -6
View File
@@ -2,20 +2,19 @@ import { readFileSync } from 'node:fs';
import { createPinia } from 'pinia';
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { expandApplicationHtml } from '../../build/application-html.js';
import { APPLICATION_VIEWS } from '../ui/views/index.js';
const HOST = /<div class="gev-view" data-gev-view="([^"]+)"><\/div>/g;
/**
* Expand application markup as the browser shows it once views are mounted:
* static templates inline, and each view rendered into its host. Markup tests
* read this instead of the raw template expansion.
* @param {string} html Document with `gev:template` markers.
* Application markup as the browser shows it once views are mounted: each
* view rendered into its host. Markup tests read this instead of the page
* source.
* @param {string} html Document with `.gev-view` hosts.
* @returns {Promise<string>}
*/
export async function renderApplicationHtml(html) {
let output = expandApplicationHtml(html);
let output = html;
for (const [host, name] of [...output.matchAll(HOST)]) {
const view = APPLICATION_VIEWS[name];
if (!view) throw new Error(`Unknown application view: ${name}`);
+14 -28
View File
@@ -1,41 +1,27 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
expandApplicationHtml,
APPLICATION_TEMPLATES,
} from '../../build/application-html.js';
import { APPLICATION_VIEW_NAMES } from '../../build/applicationViews.js';
import { APPLICATION_VIEWS } from '../ui/views/index.js';
import { readApplicationHtml } from '../testSupport/applicationHtml.mjs';
test('the standalone document expands every component once and preserves unique element ids', async () => {
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
test('the standalone document hosts every view once and keeps element ids unique', async () => {
const hosts = [...source.matchAll(/data-gev-view="([^"]+)"/g)].map((match) => match[1]);
// Order is stacking order for fixed elements that share a z-index.
assert.deepEqual(hosts, ['scene-chrome', 'cockpit', 'display-controls', 'command-dock', 'layer-panels', 'context', 'welcome', 'provider-settings']);
assert.deepEqual([...hosts].sort(), Object.keys(APPLICATION_VIEWS).sort());
const html = await readApplicationHtml();
assert.equal([...source.matchAll(/gev:template /g)].length, APPLICATION_TEMPLATES.length);
assert.doesNotMatch(html, /gev:template/);
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map(match => match[1]);
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
assert.equal(new Set(ids).size, ids.length);
assert.match(html, /id="cesiumContainer"/);
assert.match(html, /type="module" src="\/src\/main.js"/);
});
test('component selection includes only requested markup and refuses filesystem traversal', () => {
const html = expandApplicationHtml('<!-- gev:template hud-loading -->\n');
assert.match(html, /id="loading-screen"/);
assert.doesNotMatch(html, /id="cesiumContainer"/);
assert.throws(() => expandApplicationHtml('<!-- gev:template ../../.env -->'), /Unknown application template/);
});
test('views expand to mount hosts, the loading screen stays static, and every view is registered', () => {
assert.deepEqual(Object.keys(APPLICATION_VIEWS).sort(), [...APPLICATION_VIEW_NAMES].sort());
for (const name of APPLICATION_VIEW_NAMES) {
assert.ok(APPLICATION_TEMPLATES.includes(name), name);
assert.equal(
expandApplicationHtml(`<!-- gev:template ${name} -->\n`),
`<div class="gev-view" data-gev-view="${name}"></div>\n`,
);
}
// It paints before any script runs.
assert.ok(!APPLICATION_VIEW_NAMES.includes('hud-loading'));
test('the loading screen is static markup, so it paints before any script runs', () => {
const loading = source.indexOf('id="loading-screen"');
assert.ok(loading > 0);
assert.ok(loading < source.indexOf('type="module" src="/src/main.js"'));
assert.match(source, /class="loader-status" data-i18n>Initializing photorealistic world\.\.\.</);
assert.doesNotMatch(source, /gev:template/);
});
+4 -4
View File
@@ -29,8 +29,8 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
assert.equal(config.plugins[0].enforce, 'pre');
assert.equal(config.plugins[1].name, 'host-check');
assert.equal(config.plugins[1].enforce, 'pre');
assert.equal(config.plugins[4].name, 'vite:vue');
assert.equal(config.plugins[5], plugin);
assert.equal(config.plugins[3].name, 'vite:vue');
assert.equal(config.plugins[4], plugin);
assert.equal(config.server.host, 'localhost');
assert.equal(config.server.port, 4173);
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
@@ -104,7 +104,7 @@ test('build helper does not discover environment values or construct local provi
undefined,
);
assert.deepEqual(
config.plugins.slice(5).map((plugin) => plugin.name),
config.plugins.slice(4).map((plugin) => plugin.name),
['embed-framing', 'panel-build'],
);
} finally {
@@ -118,7 +118,7 @@ test('root config retains existing named exports and standalone provider order',
assert.equal(compatibility[name], value, name);
const config = standaloneConfig({ mode: 'test' });
const standaloneProviders = config.plugins
.slice(5, -6)
.slice(4, -6)
.map((plugin) => plugin.name);
assert.equal(
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
-11
View File
@@ -1,11 +0,0 @@
<!-- Intelligence HUD Overlay -->
<div id="intel-hud"></div>
<!-- Loading screen -->
<div id="loading-screen">
<div class="loader-content">
<span class="loader-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span>
<h2 translate="no">GOD'S EYE <span class="title-accent">VIEW</span></h2>
<p class="loader-status" data-i18n>Initializing photorealistic world...</p>
</div>
</div>
+28 -10
View File
@@ -10,8 +10,9 @@ import SceneChromeView from './SceneChromeView.vue';
import ProviderSettingsView from './ProviderSettingsView.vue';
/**
* Application views by template name (build/applicationViews.js lists the
* same names). Each renders the markup its static template held.
* Application views by name. A page places each one with a host element,
* `<div class="gev-view" data-gev-view="NAME"></div>` (`display: contents`),
* where the view's markup belongs.
*/
export const APPLICATION_VIEWS = Object.freeze({
'provider-settings': ProviderSettingsView,
@@ -24,21 +25,38 @@ export const APPLICATION_VIEWS = Object.freeze({
welcome: WelcomeView,
});
const mounted = new WeakSet();
const mounted = new WeakMap();
/**
* Render every application view into its host. Views belong to the page, as
* their static markup did, so they are never unmounted. Call before anything
* their static markup did, so they are unmounted only when a later call omits
* them; omit views before any control code is wired to them. Call before anything
* reads or translates the page; repeated calls are harmless.
* @param {Document} [doc] Page document.
* @param {{omit?: string[]}} [options] Views this page leaves out; their
* hosts are removed, unmounting a view an earlier call rendered.
*/
export function mountApplicationViews(doc = globalThis.document) {
export function mountApplicationViews(
doc = globalThis.document,
{ omit = [] } = {},
) {
for (const name of omit) {
if (!Object.hasOwn(APPLICATION_VIEWS, name))
throw new Error(`Unknown application view: ${name}`);
}
for (const host of doc?.querySelectorAll?.('[data-gev-view]') ?? []) {
if (mounted.has(host)) continue;
const name = host.getAttribute('data-gev-view');
const view = APPLICATION_VIEWS[name];
if (!view) throw new Error(`Unknown application view: ${name}`);
createApp(view).use(applicationPinia()).mount(host);
mounted.add(host);
if (omit.includes(name)) {
mounted.get(host)?.unmount();
mounted.delete(host);
host.remove();
continue;
}
if (mounted.has(host)) continue;
if (!Object.hasOwn(APPLICATION_VIEWS, name))
throw new Error(`Unknown application view: ${name}`);
const app = createApp(APPLICATION_VIEWS[name]).use(applicationPinia());
app.mount(host);
mounted.set(host, app);
}
}
@@ -0,0 +1,77 @@
// src/ui/views/mountApplicationViews.test.mjs — a page's view hosts render
// their views, and an application can leave views out, over happy-dom.
import '../../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createApplication } from '../../app/application.js';
import { mountApplicationViews } from './index.js';
function page(names) {
document.body.replaceChildren();
for (const name of names) {
const host = document.createElement('div');
host.className = 'gev-view';
host.setAttribute('data-gev-view', name);
document.body.append(host);
}
}
const hostNames = () =>
[...document.querySelectorAll('[data-gev-view]')].map((host) =>
host.getAttribute('data-gev-view'),
);
test('omitted views lose their hosts and the rest render', () => {
page(['welcome', 'provider-settings']);
mountApplicationViews(document, { omit: ['provider-settings'] });
assert.deepEqual(hostNames(), ['welcome']);
assert.equal(document.getElementById('key-setup-chip'), null);
assert.ok(
document.querySelector('[data-gev-view="welcome"]').children.length,
);
});
test('a view an earlier call rendered is unmounted when a later call omits it', () => {
page(['welcome', 'provider-settings']);
mountApplicationViews(document);
assert.ok(document.getElementById('key-setup-chip'));
mountApplicationViews(document, { omit: ['provider-settings'] });
assert.deepEqual(hostNames(), ['welcome']);
assert.equal(document.getElementById('key-setup-chip'), null);
});
test('unknown view names are refused, in the page or in the omit list', () => {
page([]);
for (const name of ['toString', 'constructor'])
assert.throws(
() => mountApplicationViews(document, { omit: [name] }),
/Unknown application view/,
);
page(['toString']);
assert.throws(
() => mountApplicationViews(document),
/Unknown application view/,
);
assert.throws(
() => mountApplicationViews(document, { omit: ['nope'] }),
/Unknown application view: nope/,
);
page(['nope']);
assert.throws(
() => mountApplicationViews(document),
/Unknown application view: nope/,
);
});
test('createApplication leaves out the views its omitViews names', () => {
page(['welcome', 'provider-settings']);
const noop = () => ({});
createApplication({
createScene: noop,
createControls: noop,
createData: noop,
createTools: noop,
omitViews: ['provider-settings'],
});
assert.deepEqual(hostNames(), ['welcome']);
});