@vmz/core 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/list-client-components.js +23 -5
- package/dist/route-layout-chain.d.ts +18 -0
- package/dist/route-layout-chain.js +41 -0
- package/dist/serve-host.mjs +39 -50
- package/dist/server.js +11 -3
- package/package.json +5 -1
|
@@ -7,7 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import { readdir } from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
-
import { componentEntriesFromDeployment, mergeExplicitComponentEntries, readDeploymentDocument, } from './deployment-registry.js';
|
|
10
|
+
import { componentEntriesFromDeployment, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, readDeploymentDocument, } from './deployment-registry.js';
|
|
11
11
|
export { DEPLOYMENT_SCHEMA, readDeploymentDocument, componentEntriesFromDeployment, collectDependsOnClosure, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, loadComponentEntries, importAndRegisterComponentEntries, bootstrapComponentRegistry, } from './deployment-registry.js';
|
|
12
12
|
export { createRenderHost } from './render-host.js';
|
|
13
13
|
/**
|
|
@@ -16,14 +16,23 @@ export { createRenderHost } from './render-host.js';
|
|
|
16
16
|
* @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
|
|
17
17
|
*/
|
|
18
18
|
export async function listClientComponents(dir, opts = {}) {
|
|
19
|
-
const
|
|
19
|
+
const strict = opts.strict === true;
|
|
20
|
+
const deployment = readDeploymentDocument(dir, { strict });
|
|
20
21
|
if (deployment) {
|
|
21
|
-
return componentEntriesFromDeployment(deployment).map((e) => ({
|
|
22
|
+
return dedupeComponentEntriesByTag(componentEntriesFromDeployment(deployment).map((e) => ({
|
|
23
|
+
chunkId: e.chunkId,
|
|
24
|
+
name: e.name,
|
|
25
|
+
entry: e.entry,
|
|
26
|
+
source: e.source,
|
|
27
|
+
})), { strict }).map((e) => ({
|
|
22
28
|
name: e.name,
|
|
23
29
|
entry: e.entry,
|
|
24
30
|
chunkId: e.chunkId,
|
|
25
31
|
}));
|
|
26
32
|
}
|
|
33
|
+
if (strict) {
|
|
34
|
+
throw new Error(`vmz: missing vmz-deployment.json under ${dir} (strict deployment mode)`);
|
|
35
|
+
}
|
|
27
36
|
const folder = path.join(dir, 'components');
|
|
28
37
|
/** @type {string[]} */
|
|
29
38
|
let files = [];
|
|
@@ -48,14 +57,23 @@ export async function listClientComponents(dir, opts = {}) {
|
|
|
48
57
|
* @returns {Array<{ name: string, entry: string, chunkId?: string }>}
|
|
49
58
|
*/
|
|
50
59
|
export function listClientComponentsSync(dir, opts = {}) {
|
|
51
|
-
const
|
|
60
|
+
const strict = opts.strict === true;
|
|
61
|
+
const deployment = readDeploymentDocument(dir, { strict });
|
|
52
62
|
if (deployment) {
|
|
53
|
-
return componentEntriesFromDeployment(deployment).map((e) => ({
|
|
63
|
+
return dedupeComponentEntriesByTag(componentEntriesFromDeployment(deployment).map((e) => ({
|
|
64
|
+
chunkId: e.chunkId,
|
|
65
|
+
name: e.name,
|
|
66
|
+
entry: e.entry,
|
|
67
|
+
source: e.source,
|
|
68
|
+
})), { strict }).map((e) => ({
|
|
54
69
|
name: e.name,
|
|
55
70
|
entry: e.entry,
|
|
56
71
|
chunkId: e.chunkId,
|
|
57
72
|
}));
|
|
58
73
|
}
|
|
74
|
+
if (strict) {
|
|
75
|
+
throw new Error(`vmz: missing vmz-deployment.json under ${dir} (strict deployment mode)`);
|
|
76
|
+
}
|
|
59
77
|
const folder = path.join(dir, 'components');
|
|
60
78
|
/** @type {string[]} */
|
|
61
79
|
let files = [];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-route layout chain: Application shell (outermost) + nested page Layout components.
|
|
3
|
+
*/
|
|
4
|
+
/** Chunk id for `src/Application.vmz` emit (`Application.client.js`). */
|
|
5
|
+
export declare const APPLICATION_SHELL_CHUNK = "Application";
|
|
6
|
+
/**
|
|
7
|
+
* True when the compile output includes a root Application shell.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasApplicationShell(distDir: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Nearest page Layout.client.js walking up from the page chunk (outer to inner).
|
|
12
|
+
* Does not include Application — use resolveRouteLayoutChain.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveNestedLayoutChain(distDir: string, pageChunkId: string): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Full SSR / hydrate layout chain: optional Application shell, then nested page layouts.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveRouteLayoutChain(distDir: string, pageChunkId: string): string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-route layout chain: Application shell (outermost) + nested page Layout components.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
/** Chunk id for `src/Application.vmz` emit (`Application.client.js`). */
|
|
7
|
+
export const APPLICATION_SHELL_CHUNK = 'Application';
|
|
8
|
+
/**
|
|
9
|
+
* True when the compile output includes a root Application shell.
|
|
10
|
+
*/
|
|
11
|
+
export function hasApplicationShell(distDir) {
|
|
12
|
+
return existsSync(path.join(distDir, `${APPLICATION_SHELL_CHUNK}.client.js`));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Nearest page Layout.client.js walking up from the page chunk (outer to inner).
|
|
16
|
+
* Does not include Application — use resolveRouteLayoutChain.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveNestedLayoutChain(distDir, pageChunkId) {
|
|
19
|
+
const rel = pageChunkId.replace(/^pages\//, '');
|
|
20
|
+
const parts = rel.split('/').filter(Boolean);
|
|
21
|
+
parts.pop();
|
|
22
|
+
const chain = [];
|
|
23
|
+
for (let i = parts.length; i >= 0; i--) {
|
|
24
|
+
const dirParts = parts.slice(0, i);
|
|
25
|
+
const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
|
|
26
|
+
if (existsSync(path.join(distDir, `${layoutChunk}.client.js`))) {
|
|
27
|
+
chain.unshift(layoutChunk);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return chain;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Full SSR / hydrate layout chain: optional Application shell, then nested page layouts.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveRouteLayoutChain(distDir, pageChunkId) {
|
|
36
|
+
const chain = resolveNestedLayoutChain(distDir, pageChunkId);
|
|
37
|
+
if (hasApplicationShell(distDir)) {
|
|
38
|
+
chain.unshift(APPLICATION_SHELL_CHUNK);
|
|
39
|
+
}
|
|
40
|
+
return chain;
|
|
41
|
+
}
|
package/dist/serve-host.mjs
CHANGED
|
@@ -22,8 +22,9 @@ import http from 'node:http';
|
|
|
22
22
|
import { createRequire, registerHooks } from 'node:module';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
-
import {
|
|
25
|
+
import { createRenderHost } from './render-host.js';
|
|
26
26
|
import { listClientComponents } from './list-client-components.js';
|
|
27
|
+
import { resolveRouteLayoutChain } from './route-layout-chain.js';
|
|
27
28
|
import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
|
|
28
29
|
const require = createRequire(import.meta.url);
|
|
29
30
|
const distDir = process.env.VMZ_DIST ? path.resolve(process.env.VMZ_DIST) : path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -76,6 +77,8 @@ if (isDev) {
|
|
|
76
77
|
}
|
|
77
78
|
/** @type {number} */
|
|
78
79
|
let reloadToken = Date.now();
|
|
80
|
+
/** @type {Awaited<ReturnType<typeof createRenderHost>> | null} */
|
|
81
|
+
let ssrRenderHost = null;
|
|
79
82
|
/** @type {string | null} Correlatable build id from vmz dev (Living §12.8). */
|
|
80
83
|
let lastDevBuildId = null;
|
|
81
84
|
/** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
@@ -84,6 +87,8 @@ let pageCatalog = [];
|
|
|
84
87
|
const pageCtors = new Map();
|
|
85
88
|
/** Stylesheet from deployment `cssEntry` (e.g. vmz.css). */
|
|
86
89
|
let cssEntry = null;
|
|
90
|
+
/** Fingerprint of style inputs — busts `@import` siblings when tokens change (VMZ-8). */
|
|
91
|
+
let styleBundleHash = null;
|
|
87
92
|
/** @type {{ defaultThemeId: string, themeIds: string[], activationAttr: string, contentHash: string|null } | null} */
|
|
88
93
|
let styleTheme = null;
|
|
89
94
|
/** Locale route realization artifact from `_vmz/locale-route-realization.json` (optional). */
|
|
@@ -277,7 +282,7 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
277
282
|
const resumeEntries = await loadPageResumeEntries(distDir, match.chunkId);
|
|
278
283
|
const strategies = resumeEntries.map((e) => e.strategy);
|
|
279
284
|
const eventOnlyShell = isEventOnlyShell(strategies);
|
|
280
|
-
const layoutChain =
|
|
285
|
+
const layoutChain = resolveRouteLayoutChain(distDir, match.chunkId);
|
|
281
286
|
return {
|
|
282
287
|
status,
|
|
283
288
|
stream: emitPageHtml(Page, match.chunkId, eventOnlyShell, props, opts, layoutChain, localeCtx),
|
|
@@ -503,9 +508,17 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
503
508
|
const pageDocMeta = resolvePageDocumentMeta(Page);
|
|
504
509
|
const prevLocaleHint = globalThis.__vmzLocaleIdHint;
|
|
505
510
|
globalThis.__vmzLocaleIdHint = localeId;
|
|
511
|
+
if (!ssrRenderHost) {
|
|
512
|
+
ssrRenderHost = await createRenderHost(distDir, {
|
|
513
|
+
strictDeployment: !isDev,
|
|
514
|
+
preload: 'none',
|
|
515
|
+
cacheBust: reloadToken,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
await ssrRenderHost.ensureComponents([chunkId, ...layoutChain]);
|
|
506
519
|
let bodyHtml = '';
|
|
507
520
|
try {
|
|
508
|
-
for await (const chunk of renderToStream(Page, props, { signal })) {
|
|
521
|
+
for await (const chunk of ssrRenderHost.renderToStream(Page, props, { signal })) {
|
|
509
522
|
if (signal?.aborted)
|
|
510
523
|
return;
|
|
511
524
|
bodyHtml += chunk;
|
|
@@ -517,7 +530,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
517
530
|
const Layout = await loadPageCtor(layoutChain[i]);
|
|
518
531
|
if (!Layout)
|
|
519
532
|
continue;
|
|
520
|
-
bodyHtml = await renderToString(Layout, {}, { signal, slotHtml: bodyHtml });
|
|
533
|
+
bodyHtml = await ssrRenderHost.renderToString(Layout, {}, { signal, slotHtml: bodyHtml });
|
|
521
534
|
if (signal?.aborted)
|
|
522
535
|
return;
|
|
523
536
|
}
|
|
@@ -539,7 +552,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
539
552
|
throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
|
|
540
553
|
}
|
|
541
554
|
const entrySrc = `/${eventOnlyShell ? 'entry-event.js' : 'entry-client.js'}?t=${reloadToken}`;
|
|
542
|
-
const cssHref =
|
|
555
|
+
const cssHref = cssEntryWithBust(cssEntry);
|
|
543
556
|
yield native.generatePageShell({
|
|
544
557
|
bodyHtml,
|
|
545
558
|
chunkId,
|
|
@@ -709,28 +722,18 @@ async function softReload(opts = {}) {
|
|
|
709
722
|
catch {
|
|
710
723
|
localeArtifact = null;
|
|
711
724
|
}
|
|
712
|
-
const componentEntries = await listClientComponents(distDir);
|
|
725
|
+
const componentEntries = await listClientComponents(distDir, { strict: !isDev });
|
|
726
|
+
ssrRenderHost = await createRenderHost(distDir, {
|
|
727
|
+
strictDeployment: !isDev,
|
|
728
|
+
preload: 'none',
|
|
729
|
+
cacheBust: nextToken,
|
|
730
|
+
});
|
|
713
731
|
const nextCatalog = await listPageClientFiles(distDir);
|
|
714
732
|
if (!nextCatalog.length) {
|
|
715
733
|
throw new Error(`vmz serve: no pages/**/*.client.js in ${distDir}`);
|
|
716
734
|
}
|
|
717
|
-
/** @type {Record<string, any>} */
|
|
718
|
-
const components = {};
|
|
719
735
|
/** @type {Map<string, any>} */
|
|
720
736
|
const nextCtors = new Map();
|
|
721
|
-
const affectedNames = new Set(affected
|
|
722
|
-
.map((c) => String(c))
|
|
723
|
-
.filter((c) => c.startsWith('components/') || !c.includes('/'))
|
|
724
|
-
.map((c) => c.split('/').pop())
|
|
725
|
-
.filter(Boolean));
|
|
726
|
-
for (const entry of componentEntries) {
|
|
727
|
-
if (islandHmr && affectedNames.size > 0 && !affectedNames.has(entry.name)) {
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
const href = bustUrl(pathToFileURL(path.join(distDir, entry.entry)).href);
|
|
731
|
-
const mod = await import(href);
|
|
732
|
-
components[entry.name] = mod.default;
|
|
733
|
-
}
|
|
734
737
|
if (!islandHmr) {
|
|
735
738
|
const pagesToLoad = reloadAllPages ? nextCatalog : nextCatalog.filter((p) => pageNeedsReload(p.chunkId, affected));
|
|
736
739
|
for (const p of pagesToLoad) {
|
|
@@ -758,13 +761,11 @@ async function softReload(opts = {}) {
|
|
|
758
761
|
}
|
|
759
762
|
}
|
|
760
763
|
}
|
|
761
|
-
if (Object.keys(components).length) {
|
|
762
|
-
registerComponents(components);
|
|
763
|
-
}
|
|
764
764
|
const indexChunk = pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || pageCatalog[0].chunkId;
|
|
765
765
|
const resumeEntries = await loadPageResumeEntries(distDir, indexChunk);
|
|
766
766
|
const styleMeta = await loadDeploymentStyle(distDir);
|
|
767
767
|
cssEntry = styleMeta.cssEntry;
|
|
768
|
+
styleBundleHash = styleMeta.styleBundleHash;
|
|
768
769
|
styleTheme = styleMeta.styleTheme;
|
|
769
770
|
const lazyEventNames = resumeEntries
|
|
770
771
|
.filter((e) => isEventStrategy(e.strategy))
|
|
@@ -1205,32 +1206,6 @@ function isRouteGroupDir(seg) {
|
|
|
1205
1206
|
function isRouteBoundaryStem(stem) {
|
|
1206
1207
|
return stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound';
|
|
1207
1208
|
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Nearest `Layout.client.js` walking up from the page chunk (outer→inner).
|
|
1210
|
-
* @param {string} pageChunkId
|
|
1211
|
-
* @returns {string[]}
|
|
1212
|
-
*/
|
|
1213
|
-
function resolveLayoutChain(pageChunkId) {
|
|
1214
|
-
const rel = pageChunkId.replace(/^pages\//, '');
|
|
1215
|
-
const parts = rel.split('/').filter(Boolean);
|
|
1216
|
-
parts.pop(); // page stem
|
|
1217
|
-
/** @type {string[]} */
|
|
1218
|
-
const chain = [];
|
|
1219
|
-
for (let i = parts.length; i >= 0; i--) {
|
|
1220
|
-
const dirParts = parts.slice(0, i);
|
|
1221
|
-
const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
|
|
1222
|
-
const abs = path.join(distDir, `${layoutChunk}.client.js`);
|
|
1223
|
-
try {
|
|
1224
|
-
// sync existence — layouts are compile artifacts next to pages
|
|
1225
|
-
if (existsSync(abs))
|
|
1226
|
-
chain.unshift(layoutChunk);
|
|
1227
|
-
}
|
|
1228
|
-
catch {
|
|
1229
|
-
/* ignore */
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
return chain;
|
|
1233
|
-
}
|
|
1234
1209
|
/**
|
|
1235
1210
|
* @param {string} pathname
|
|
1236
1211
|
* @param {typeof pageCatalog} catalog
|
|
@@ -1442,6 +1417,20 @@ function requireNativeGenerator() {
|
|
|
1442
1417
|
const THEME_STORE_KEY = 'vmz-theme';
|
|
1443
1418
|
/** Host preference key for `routing.strategy: 'none'` (cookie + localStorage). */
|
|
1444
1419
|
const LOCALE_STORE_KEY = 'vmz.locale';
|
|
1420
|
+
/**
|
|
1421
|
+
* Cache-bust stylesheet entry for dev reload (token + serve revision).
|
|
1422
|
+
* @param {string | null | undefined} entry
|
|
1423
|
+
*/
|
|
1424
|
+
function cssEntryWithBust(entry) {
|
|
1425
|
+
if (!entry)
|
|
1426
|
+
return undefined;
|
|
1427
|
+
const base = String(entry).replace(/^\/+/, '');
|
|
1428
|
+
const params = new URLSearchParams();
|
|
1429
|
+
params.set('t', String(reloadToken));
|
|
1430
|
+
if (styleBundleHash)
|
|
1431
|
+
params.set('h', styleBundleHash);
|
|
1432
|
+
return `${base}?${params.toString()}`;
|
|
1433
|
+
}
|
|
1445
1434
|
/**
|
|
1446
1435
|
* @param {string} dir
|
|
1447
1436
|
* @returns {Promise<{ cssEntry: string|null, styleTheme: typeof styleTheme, styleBundleHash: string|null }>}
|
package/dist/server.js
CHANGED
|
@@ -231,7 +231,9 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
231
231
|
if (verb === 'GET' && opts.distDir) {
|
|
232
232
|
const nodePath = await import('node:path');
|
|
233
233
|
const { readFile, stat } = await import('node:fs/promises');
|
|
234
|
-
const file = await resolveDistStatic(opts.distDir, url.pathname, nodePath, stat
|
|
234
|
+
const file = await resolveDistStatic(opts.distDir, url.pathname, nodePath, stat, {
|
|
235
|
+
cookieHeader: String(req.headers.cookie || ''),
|
|
236
|
+
});
|
|
235
237
|
const hasSsr = typeof opts.renderPageStream === 'function' ||
|
|
236
238
|
typeof opts.renderPage === 'function' ||
|
|
237
239
|
typeof opts.renderIndexStream === 'function' ||
|
|
@@ -749,9 +751,15 @@ async function sendHtmlStream(res, status, source, signal) {
|
|
|
749
751
|
* @param {string} type
|
|
750
752
|
*/
|
|
751
753
|
function sendBytes(res, status, body, type) {
|
|
752
|
-
|
|
754
|
+
/** @type {Record<string, string | number>} */
|
|
755
|
+
const headers = {
|
|
753
756
|
'content-type': type,
|
|
754
757
|
'content-length': body.byteLength,
|
|
755
|
-
}
|
|
758
|
+
};
|
|
759
|
+
// Dev: stylesheets are rebuilt in-place — never cache @import siblings (VMZ-8).
|
|
760
|
+
if (process.env.VMZ_DEV === '1' && typeof type === 'string' && type.startsWith('text/css')) {
|
|
761
|
+
headers['cache-control'] = 'no-store';
|
|
762
|
+
}
|
|
763
|
+
res.writeHead(status, headers);
|
|
756
764
|
res.end(body);
|
|
757
765
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
|
|
6
6
|
"exports": {
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./render-host": {
|
|
36
36
|
"types": "./dist/render-host.d.ts",
|
|
37
37
|
"default": "./dist/render-host.js"
|
|
38
|
+
},
|
|
39
|
+
"./route-layout-chain": {
|
|
40
|
+
"types": "./dist/route-layout-chain.d.ts",
|
|
41
|
+
"default": "./dist/route-layout-chain.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"files": [
|