feat(ui): render application templates as Vue views mounted before wiring

Templates listed in build/applicationViews.js expand to mount hosts, and
mountApplicationViews() renders them before translation and wiring. Markup
tests read the page through a server-side render helper, the i18n scanner
reads view templates, and Vue preserves whitespace so views keep the exact
DOM. No template is converted yet.
This commit is contained in:
Sameh Khamis committed 2026-10-30 17:23:45 -07:00
1 parent b7e7058a5c
commit 9d24945143
32 files changed
+243 -63

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
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.
* @returns {Promise<string>}
*/
export async function renderApplicationHtml(html) {
let output = expandApplicationHtml(html);
for (const [host, name] of [...output.matchAll(HOST)]) {
const view = APPLICATION_VIEWS[name];
if (!view) throw new Error(`Unknown application view: ${name}`);
const markup = (await renderToString(createSSRApp(view).use(createPinia())))
// Hydration markers and empty-attribute values are SSR detail; the
// DOM is the same as the template's.
.replace(/<!--\[-->|<!--\]-->|<!---->/g, '')
.replace(/ ([\w:.-]+)=""/g, ' $1');
output = output.replace(
host,
`<div class="gev-view" data-gev-view="${name}">${markup}</div>`,
);
}
return output;
}
/** The standalone page's markup with every view rendered. */
export function readApplicationHtml() {
return renderApplicationHtml(
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
);
}
+7 -1
View File
@@ -1,6 +1,7 @@
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { compileScript, compileTemplate, parse } from 'vue/compiler-sfc';
import { VUE_COMPILER_OPTIONS } from '../../build/vueOptions.js';
/**
* Compile a single-file component to an ES module for Node's test runner.
@@ -11,7 +12,11 @@ export function compileVueModule(source, filename) {
if (errors.length) throw errors[0];
const id = filename;
if (descriptor.scriptSetup) {
return compileScript(descriptor, { id, inlineTemplate: true }).content;
return compileScript(descriptor, {
id,
inlineTemplate: true,
templateOptions: { compilerOptions: VUE_COMPILER_OPTIONS },
}).content;
}
const script = descriptor.script
? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content
@@ -22,6 +27,7 @@ export function compileVueModule(source, filename) {
source: descriptor.template.content,
filename,
id,
compilerOptions: VUE_COMPILER_OPTIONS,
});
if (compiled.errors.length) throw compiled.errors[0];
template = compiled.code;