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

+2 -1
View File
@@ -2,6 +2,7 @@ import { readFile, realpath } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import vue from '@vitejs/plugin-vue';
import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js';
import { build, normalizePath } from 'vite';
/** Build each browser or Node export group and reject imports outside its declared ownership. */
@@ -100,7 +101,7 @@ export async function checkPackageBoundaries(root) {
ssr: node ? { noExternal: true } : undefined,
logLevel: 'silent',
plugins: [
vue(),
vue(VUE_PLUGIN_OPTIONS),
{
name: 'check-package-ownership',
moduleParsed(info) {
+22 -6
View File
@@ -119,10 +119,10 @@ export function extractMessages() {
}
}
const markup = trackedFiles('index.html', 'src/ui/templates/*.html');
const markup = markupFiles();
const TAG = /<([a-zA-Z][\w-]*)(\s[^>]*?)\/?>([^<]*)/gs;
for (const file of markup) {
const html = readFileSync(path.join(ROOT, file), 'utf8');
const html = readMarkup(file);
for (const [, tag, attrs, text] of html.matchAll(TAG)) {
const where = `${file} <${tag}>`;
if (/\sdata-i18n(\s|=|$)/.test(attrs)) {
@@ -404,10 +404,8 @@ export function unmarkedScript(file, original) {
*/
export function findUnmarked() {
const found = [];
for (const file of trackedFiles('index.html', 'src/ui/templates/*.html'))
found.push(
...unmarkedMarkup(file, readFileSync(path.join(ROOT, file), 'utf8')),
);
for (const file of markupFiles())
found.push(...unmarkedMarkup(file, readMarkup(file)));
for (const file of sourceFiles().filter(
(name) => !NOT_UI.some((pattern) => pattern.test(name)),
))
@@ -423,6 +421,24 @@ export function findUnmarked() {
});
}
/** Static markup opted in with data-i18n*: the page, its templates and views. */
function markupFiles() {
return trackedFiles(
'index.html',
'src/ui/templates/*.html',
'src/ui/views/*.vue',
);
}
/** A file's markup; for a single-file component, its template block. */
function readMarkup(file) {
const source = readFileSync(path.join(ROOT, file), 'utf8');
if (!file.endsWith('.vue')) return source;
const start = source.indexOf('<template>');
const end = source.lastIndexOf('</template>');
return start === -1 ? '' : source.slice(start + '<template>'.length, end);
}
// Text that never reaches the page as UI: tool results and descriptions for
// AI clients (MCP and the voice model).
const NOT_UI = [/^src\/tools\//];
+9 -3
View File
@@ -17,8 +17,12 @@
},
"application": {
"exports": ["./application"],
"modules": ["src/app/application.js"],
"external": []
"modules": [
"src/app/application.js",
"src/ui/vueRuntime.js",
"src/ui/views/index.js"
],
"external": ["vue", "pinia"]
},
"viewer": {
"exports": ["./application/viewer"],
@@ -40,7 +44,9 @@
"build/application-html.js",
"build/allowedHosts.js",
"build/embed-framing.js",
"build/panel.js"
"build/panel.js",
"build/applicationViews.js",
"build/vueOptions.js"
],
"external": ["vite-plugin-cesium", "@vitejs/plugin-vue"]
},