@vmz/core 0.1.11 → 0.1.13
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/deployment-registry.d.ts +2 -2
- package/dist/deployment-registry.js +16 -51
- package/dist/dom-core.js +8 -0
- package/dist/list-client-components.js +2 -2
- package/dist/native-addon.d.ts +12 -0
- package/dist/native-addon.js +116 -0
- package/dist/route-layout-chain.d.ts +18 -0
- package/dist/route-layout-chain.js +41 -0
- package/dist/serve-host.mjs +85 -108
- package/dist/server.js +8 -2
- package/package.json +13 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deployment graph → component registry bootstrap (shared by all SSR/DOM hosts).
|
|
3
|
-
*
|
|
3
|
+
* Parse/validate and graph queries delegate to Rust vmz-artifacts via N-API.
|
|
4
4
|
*/
|
|
5
5
|
export declare const DEPLOYMENT_SCHEMA = "vmz.deployment.v0";
|
|
6
6
|
/**
|
|
@@ -13,7 +13,7 @@ export declare function readDeploymentDocument(distDir: any, opts?: {}): any;
|
|
|
13
13
|
* @param {any} deployment
|
|
14
14
|
* @returns {Array<{ chunkId: string, name: string, entry: string, source: string }>}
|
|
15
15
|
*/
|
|
16
|
-
export declare function componentEntriesFromDeployment(deployment: any): any
|
|
16
|
+
export declare function componentEntriesFromDeployment(deployment: any): any;
|
|
17
17
|
/**
|
|
18
18
|
* @param {any} deployment
|
|
19
19
|
* @param {string[]} rootChunkIds
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
3
|
* Deployment graph → component registry bootstrap (shared by all SSR/DOM hosts).
|
|
4
|
-
*
|
|
4
|
+
* Parse/validate and graph queries delegate to Rust vmz-artifacts via N-API.
|
|
5
5
|
*/
|
|
6
6
|
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 { requireNativeFn } from './native-addon.js';
|
|
10
11
|
export const DEPLOYMENT_SCHEMA = 'vmz.deployment.v0';
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} jsonText
|
|
14
|
+
* @returns {any}
|
|
15
|
+
*/
|
|
16
|
+
function parseDeploymentJson(jsonText) {
|
|
17
|
+
requireNativeFn('deploymentValidate')(jsonText);
|
|
18
|
+
return JSON.parse(jsonText);
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* @param {string} distDir
|
|
13
22
|
* @param {{ strict?: boolean }} [opts]
|
|
@@ -31,45 +40,22 @@ export function readDeploymentDocument(distDir, opts = {}) {
|
|
|
31
40
|
throw new Error(`vmz: cannot read vmz-deployment.json: ${e instanceof Error ? e.message : e}`);
|
|
32
41
|
return null;
|
|
33
42
|
}
|
|
34
|
-
let doc;
|
|
35
43
|
try {
|
|
36
|
-
|
|
44
|
+
return parseDeploymentJson(raw);
|
|
37
45
|
}
|
|
38
46
|
catch (e) {
|
|
39
47
|
if (strict)
|
|
40
48
|
throw new Error(`vmz: invalid vmz-deployment.json: ${e instanceof Error ? e.message : e}`);
|
|
41
49
|
return null;
|
|
42
50
|
}
|
|
43
|
-
if (doc.schema !== DEPLOYMENT_SCHEMA) {
|
|
44
|
-
if (strict)
|
|
45
|
-
throw new Error(`vmz: unsupported deployment schema ${doc.schema}`);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return doc;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* @param {any} deployment
|
|
52
54
|
* @returns {Array<{ chunkId: string, name: string, entry: string, source: string }>}
|
|
53
55
|
*/
|
|
54
56
|
export function componentEntriesFromDeployment(deployment) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const unit of deployment.units || []) {
|
|
58
|
-
if (unit?.kind !== 'component')
|
|
59
|
-
continue;
|
|
60
|
-
const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
61
|
-
const name = chunkId.split('/').pop();
|
|
62
|
-
if (!name)
|
|
63
|
-
continue;
|
|
64
|
-
const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
65
|
-
out.push({
|
|
66
|
-
chunkId,
|
|
67
|
-
name,
|
|
68
|
-
entry,
|
|
69
|
-
source: String(unit.source || ''),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return out.sort((a, b) => a.chunkId.localeCompare(b.chunkId));
|
|
57
|
+
const json = JSON.stringify(deployment);
|
|
58
|
+
return requireNativeFn('deploymentComponentEntries')(json);
|
|
73
59
|
}
|
|
74
60
|
/**
|
|
75
61
|
* @param {any} deployment
|
|
@@ -77,30 +63,9 @@ export function componentEntriesFromDeployment(deployment) {
|
|
|
77
63
|
* @returns {Set<string>}
|
|
78
64
|
*/
|
|
79
65
|
export function collectDependsOnClosure(deployment, rootChunkIds) {
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
byId.set(String(unit.chunkId || '').replace(/\\/g, '/'), unit);
|
|
84
|
-
}
|
|
85
|
-
/** @type {Set<string>} */
|
|
86
|
-
const out = new Set();
|
|
87
|
-
/** @type {string[]} */
|
|
88
|
-
const stack = rootChunkIds.map((id) => String(id).replace(/\\/g, '/')).filter(Boolean);
|
|
89
|
-
while (stack.length) {
|
|
90
|
-
const id = stack.pop();
|
|
91
|
-
if (!id || out.has(id))
|
|
92
|
-
continue;
|
|
93
|
-
out.add(id);
|
|
94
|
-
const unit = byId.get(id);
|
|
95
|
-
if (!unit)
|
|
96
|
-
continue;
|
|
97
|
-
for (const dep of unit.dependsOn || []) {
|
|
98
|
-
const d = String(dep).replace(/\\/g, '/');
|
|
99
|
-
if (!out.has(d))
|
|
100
|
-
stack.push(d);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return out;
|
|
66
|
+
const json = JSON.stringify(deployment);
|
|
67
|
+
const ids = requireNativeFn('deploymentDependsOnClosure')(json, rootChunkIds);
|
|
68
|
+
return new Set(ids);
|
|
104
69
|
}
|
|
105
70
|
/**
|
|
106
71
|
* Resolve tag conflicts; strict mode throws, dev mode warns and keeps last chunkId.
|
package/dist/dom-core.js
CHANGED
|
@@ -498,6 +498,14 @@ export const directApi = {
|
|
|
498
498
|
if (typeof propName !== 'string' || !propName || propName.startsWith('#'))
|
|
499
499
|
return;
|
|
500
500
|
child[propName] = raw;
|
|
501
|
+
if (typeof child.__vmzOnParentProp === 'function') {
|
|
502
|
+
try {
|
|
503
|
+
child.__vmzOnParentProp(propName, raw);
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
console.error('vmz:dom __vmzOnParentProp', err);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
501
509
|
scheduleRefresh(child, { type: 'replace', root: propName });
|
|
502
510
|
});
|
|
503
511
|
},
|
|
@@ -24,7 +24,7 @@ export async function listClientComponents(dir, opts = {}) {
|
|
|
24
24
|
name: e.name,
|
|
25
25
|
entry: e.entry,
|
|
26
26
|
source: e.source,
|
|
27
|
-
}))).map((e) => ({
|
|
27
|
+
})), { strict }).map((e) => ({
|
|
28
28
|
name: e.name,
|
|
29
29
|
entry: e.entry,
|
|
30
30
|
chunkId: e.chunkId,
|
|
@@ -65,7 +65,7 @@ export function listClientComponentsSync(dir, opts = {}) {
|
|
|
65
65
|
name: e.name,
|
|
66
66
|
entry: e.entry,
|
|
67
67
|
source: e.source,
|
|
68
|
-
}))).map((e) => ({
|
|
68
|
+
})), { strict }).map((e) => ({
|
|
69
69
|
name: e.name,
|
|
70
70
|
entry: e.entry,
|
|
71
71
|
chunkId: e.chunkId,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover and load the vmz N-API addon (same contract as `@vmz/vmz` loadNative).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @returns {any}
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadNativeAddon(): any;
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} fnName
|
|
10
|
+
* @returns {any}
|
|
11
|
+
*/
|
|
12
|
+
export declare function requireNativeFn(fnName: any): any;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Discover and load the vmz N-API addon (same contract as `@vmz/vmz` loadNative).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
/** @type {any | null | undefined} */
|
|
11
|
+
let _nativeAddon;
|
|
12
|
+
/**
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function platformTriple() {
|
|
16
|
+
const { platform, arch } = process;
|
|
17
|
+
if (platform === 'win32' && arch === 'x64')
|
|
18
|
+
return 'win32-x64-msvc';
|
|
19
|
+
if (platform === 'win32' && arch === 'arm64')
|
|
20
|
+
return 'win32-arm64-msvc';
|
|
21
|
+
if (platform === 'darwin' && arch === 'arm64')
|
|
22
|
+
return 'darwin-arm64';
|
|
23
|
+
if (platform === 'darwin' && arch === 'x64')
|
|
24
|
+
return 'darwin-x64';
|
|
25
|
+
if (platform === 'linux' && arch === 'x64')
|
|
26
|
+
return 'linux-x64-gnu';
|
|
27
|
+
if (platform === 'linux' && arch === 'arm64')
|
|
28
|
+
return 'linux-arm64-gnu';
|
|
29
|
+
return `${platform}-${arch}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} triple
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function platformShort(triple) {
|
|
36
|
+
if (triple === 'win32-x64-msvc')
|
|
37
|
+
return 'win32-x64';
|
|
38
|
+
if (triple === 'win32-arm64-msvc')
|
|
39
|
+
return 'win32-arm64';
|
|
40
|
+
if (triple === 'linux-x64-gnu')
|
|
41
|
+
return 'linux-x64';
|
|
42
|
+
if (triple === 'linux-arm64-gnu')
|
|
43
|
+
return 'linux-arm64';
|
|
44
|
+
return triple;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
*/
|
|
49
|
+
function nativeCandidatePaths() {
|
|
50
|
+
const triple = platformTriple();
|
|
51
|
+
const short = platformShort(triple);
|
|
52
|
+
const name = `@vmz/vmz-${short}`;
|
|
53
|
+
/** @type {string[]} */
|
|
54
|
+
const candidates = [];
|
|
55
|
+
try {
|
|
56
|
+
const resolved = require.resolve(`${name}/package.json`);
|
|
57
|
+
const dir = path.dirname(resolved);
|
|
58
|
+
candidates.push(path.join(dir, `vmz.${triple}.node`), path.join(dir, 'vmz.node'));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* optional dep not installed */
|
|
62
|
+
}
|
|
63
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
for (let depth = 0; depth < 12; depth++) {
|
|
65
|
+
candidates.push(path.join(dir, 'node_modules', name, `vmz.${triple}.node`), path.join(dir, 'node_modules', name, 'vmz.node'));
|
|
66
|
+
const parent = path.dirname(dir);
|
|
67
|
+
if (parent === dir)
|
|
68
|
+
break;
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
return candidates;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @returns {any}
|
|
75
|
+
*/
|
|
76
|
+
export function loadNativeAddon() {
|
|
77
|
+
if (_nativeAddon !== undefined) {
|
|
78
|
+
if (!_nativeAddon) {
|
|
79
|
+
throw new Error('vmz native addon missing — run `pnpm napi:build`');
|
|
80
|
+
}
|
|
81
|
+
return _nativeAddon;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const envPath = (typeof process.env.VMZ_NATIVE_NODE === 'string' && process.env.VMZ_NATIVE_NODE.trim()) || '';
|
|
85
|
+
if (envPath) {
|
|
86
|
+
_nativeAddon = require(path.resolve(envPath));
|
|
87
|
+
return _nativeAddon;
|
|
88
|
+
}
|
|
89
|
+
for (const p of nativeCandidatePaths()) {
|
|
90
|
+
if (existsSync(p)) {
|
|
91
|
+
_nativeAddon = require(p);
|
|
92
|
+
return _nativeAddon;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
_nativeAddon = null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
_nativeAddon = null;
|
|
99
|
+
}
|
|
100
|
+
if (!_nativeAddon) {
|
|
101
|
+
throw new Error('vmz native addon missing — run `pnpm napi:build`');
|
|
102
|
+
}
|
|
103
|
+
return _nativeAddon;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} fnName
|
|
107
|
+
* @returns {any}
|
|
108
|
+
*/
|
|
109
|
+
export function requireNativeFn(fnName) {
|
|
110
|
+
const native = loadNativeAddon();
|
|
111
|
+
const fn = native[fnName];
|
|
112
|
+
if (typeof fn !== 'function') {
|
|
113
|
+
throw new Error(`vmz native addon missing ${fnName} — run \`pnpm napi:build\``);
|
|
114
|
+
}
|
|
115
|
+
return fn;
|
|
116
|
+
}
|
|
@@ -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
|
@@ -24,9 +24,70 @@ import path from 'node:path';
|
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
25
|
import { createRenderHost } from './render-host.js';
|
|
26
26
|
import { listClientComponents } from './list-client-components.js';
|
|
27
|
+
import { loadNativeAddon } from './native-addon.js';
|
|
28
|
+
import { resolveRouteLayoutChain } from './route-layout-chain.js';
|
|
27
29
|
import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
|
|
28
30
|
const require = createRequire(import.meta.url);
|
|
29
31
|
const distDir = process.env.VMZ_DIST ? path.resolve(process.env.VMZ_DIST) : path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
/** App project root for bare import / JSON resolve (dev SSR ≡ build node_modules). */
|
|
33
|
+
function projectRootForResolve() {
|
|
34
|
+
const fromEnv = typeof process.env.VMZ_PROJECT_ROOT === 'string' ? process.env.VMZ_PROJECT_ROOT.trim() : '';
|
|
35
|
+
if (fromEnv)
|
|
36
|
+
return path.resolve(fromEnv);
|
|
37
|
+
return process.cwd();
|
|
38
|
+
}
|
|
39
|
+
/** @type {ReturnType<typeof createRequire> | null} */
|
|
40
|
+
let appPackageRequire = null;
|
|
41
|
+
function appPackageRequireResolve() {
|
|
42
|
+
if (!appPackageRequire) {
|
|
43
|
+
const root = projectRootForResolve();
|
|
44
|
+
const pkg = path.join(root, 'package.json');
|
|
45
|
+
appPackageRequire = existsSync(pkg) ? createRequire(pkg) : require;
|
|
46
|
+
}
|
|
47
|
+
return appPackageRequire;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Dev/prod serve-host: resolve workspace peers + JSON imports from the app package root.
|
|
51
|
+
* Dist-relative ESM cannot see app `node_modules` without this hook.
|
|
52
|
+
*/
|
|
53
|
+
function installAppModuleResolveHooks() {
|
|
54
|
+
registerHooks({
|
|
55
|
+
resolve(specifier, context, nextResolve) {
|
|
56
|
+
if (!specifier ||
|
|
57
|
+
specifier.startsWith('.') ||
|
|
58
|
+
specifier.startsWith('node:') ||
|
|
59
|
+
specifier.startsWith('file:') ||
|
|
60
|
+
specifier.startsWith('#')) {
|
|
61
|
+
return nextResolve(specifier, context);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const resolved = appPackageRequireResolve().resolve(specifier);
|
|
65
|
+
return { url: pathToFileURL(resolved).href, shortCircuit: true };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return nextResolve(specifier, context);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
load(url, context, nextLoad) {
|
|
72
|
+
const pathOnly = url.split('?')[0].split('#')[0];
|
|
73
|
+
if (!pathOnly.endsWith('.json'))
|
|
74
|
+
return nextLoad(url, context);
|
|
75
|
+
try {
|
|
76
|
+
const filePath = fileURLToPath(pathOnly);
|
|
77
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
78
|
+
return {
|
|
79
|
+
format: 'module',
|
|
80
|
+
shortCircuit: true,
|
|
81
|
+
source: `export default ${raw}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return nextLoad(url, context);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
installAppModuleResolveHooks();
|
|
30
91
|
const host = process.env.VMZ_HOST || '127.0.0.1';
|
|
31
92
|
const port = Number(process.env.VMZ_PORT || process.env.PORT || 5173);
|
|
32
93
|
const isDev = process.env.VMZ_DEV === '1' || process.env.VMZ_DEV === 'true';
|
|
@@ -86,6 +147,8 @@ let pageCatalog = [];
|
|
|
86
147
|
const pageCtors = new Map();
|
|
87
148
|
/** Stylesheet from deployment `cssEntry` (e.g. vmz.css). */
|
|
88
149
|
let cssEntry = null;
|
|
150
|
+
/** Fingerprint of style inputs — busts `@import` siblings when tokens change (VMZ-8). */
|
|
151
|
+
let styleBundleHash = null;
|
|
89
152
|
/** @type {{ defaultThemeId: string, themeIds: string[], activationAttr: string, contentHash: string|null } | null} */
|
|
90
153
|
let styleTheme = null;
|
|
91
154
|
/** Locale route realization artifact from `_vmz/locale-route-realization.json` (optional). */
|
|
@@ -99,12 +162,6 @@ let shuttingDown = false;
|
|
|
99
162
|
let ready = false;
|
|
100
163
|
/** @type {{ message: string, stack?: string, at: number } | null} */
|
|
101
164
|
let lastDevError = null;
|
|
102
|
-
/**
|
|
103
|
-
* Native CodeGenerators handle — must be declared before top-level `await softReload()`
|
|
104
|
-
* (TDZ: requireNativeGenerator may run during that await).
|
|
105
|
-
* @type {any}
|
|
106
|
-
*/
|
|
107
|
-
let _nativeGen;
|
|
108
165
|
setServerModuleResolver((moduleId) => {
|
|
109
166
|
const rel = moduleId.replace(/^#server\//, '') + '.js';
|
|
110
167
|
return bustUrl(pathToFileURL(path.join(distDir, '#server', rel)).href);
|
|
@@ -279,7 +336,7 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
279
336
|
const resumeEntries = await loadPageResumeEntries(distDir, match.chunkId);
|
|
280
337
|
const strategies = resumeEntries.map((e) => e.strategy);
|
|
281
338
|
const eventOnlyShell = isEventOnlyShell(strategies);
|
|
282
|
-
const layoutChain =
|
|
339
|
+
const layoutChain = resolveRouteLayoutChain(distDir, match.chunkId);
|
|
283
340
|
return {
|
|
284
341
|
status,
|
|
285
342
|
stream: emitPageHtml(Page, match.chunkId, eventOnlyShell, props, opts, layoutChain, localeCtx),
|
|
@@ -338,7 +395,7 @@ function normalizeActionResult(acted) {
|
|
|
338
395
|
* @param {string} marker
|
|
339
396
|
*/
|
|
340
397
|
async function* emitAccessShell(marker) {
|
|
341
|
-
const native =
|
|
398
|
+
const native = loadNativeAddon();
|
|
342
399
|
if (typeof native.generateHtmlShell !== 'function') {
|
|
343
400
|
throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
|
|
344
401
|
}
|
|
@@ -544,12 +601,12 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
544
601
|
}
|
|
545
602
|
if (signal?.aborted)
|
|
546
603
|
return;
|
|
547
|
-
const native =
|
|
604
|
+
const native = loadNativeAddon();
|
|
548
605
|
if (typeof native.generatePageShell !== 'function') {
|
|
549
606
|
throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
|
|
550
607
|
}
|
|
551
608
|
const entrySrc = `/${eventOnlyShell ? 'entry-event.js' : 'entry-client.js'}?t=${reloadToken}`;
|
|
552
|
-
const cssHref =
|
|
609
|
+
const cssHref = cssEntryWithBust(cssEntry);
|
|
553
610
|
yield native.generatePageShell({
|
|
554
611
|
bodyHtml,
|
|
555
612
|
chunkId,
|
|
@@ -762,6 +819,7 @@ async function softReload(opts = {}) {
|
|
|
762
819
|
const resumeEntries = await loadPageResumeEntries(distDir, indexChunk);
|
|
763
820
|
const styleMeta = await loadDeploymentStyle(distDir);
|
|
764
821
|
cssEntry = styleMeta.cssEntry;
|
|
822
|
+
styleBundleHash = styleMeta.styleBundleHash;
|
|
765
823
|
styleTheme = styleMeta.styleTheme;
|
|
766
824
|
const lazyEventNames = resumeEntries
|
|
767
825
|
.filter((e) => isEventStrategy(e.strategy))
|
|
@@ -903,7 +961,7 @@ async function* emitDevErrorHtml(err) {
|
|
|
903
961
|
};
|
|
904
962
|
})();
|
|
905
963
|
</script>`;
|
|
906
|
-
const native =
|
|
964
|
+
const native = loadNativeAddon();
|
|
907
965
|
if (typeof native.generateHtmlShell !== 'function') {
|
|
908
966
|
throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
|
|
909
967
|
}
|
|
@@ -1202,32 +1260,6 @@ function isRouteGroupDir(seg) {
|
|
|
1202
1260
|
function isRouteBoundaryStem(stem) {
|
|
1203
1261
|
return stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound';
|
|
1204
1262
|
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Nearest `Layout.client.js` walking up from the page chunk (outer→inner).
|
|
1207
|
-
* @param {string} pageChunkId
|
|
1208
|
-
* @returns {string[]}
|
|
1209
|
-
*/
|
|
1210
|
-
function resolveLayoutChain(pageChunkId) {
|
|
1211
|
-
const rel = pageChunkId.replace(/^pages\//, '');
|
|
1212
|
-
const parts = rel.split('/').filter(Boolean);
|
|
1213
|
-
parts.pop(); // page stem
|
|
1214
|
-
/** @type {string[]} */
|
|
1215
|
-
const chain = [];
|
|
1216
|
-
for (let i = parts.length; i >= 0; i--) {
|
|
1217
|
-
const dirParts = parts.slice(0, i);
|
|
1218
|
-
const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
|
|
1219
|
-
const abs = path.join(distDir, `${layoutChunk}.client.js`);
|
|
1220
|
-
try {
|
|
1221
|
-
// sync existence — layouts are compile artifacts next to pages
|
|
1222
|
-
if (existsSync(abs))
|
|
1223
|
-
chain.unshift(layoutChunk);
|
|
1224
|
-
}
|
|
1225
|
-
catch {
|
|
1226
|
-
/* ignore */
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
return chain;
|
|
1230
|
-
}
|
|
1231
1263
|
/**
|
|
1232
1264
|
* @param {string} pathname
|
|
1233
1265
|
* @param {typeof pageCatalog} catalog
|
|
@@ -1345,7 +1377,7 @@ async function runRouteGate(pathname, chunkId) {
|
|
|
1345
1377
|
*/
|
|
1346
1378
|
function emitEntryClient(eager, lazy, token) {
|
|
1347
1379
|
const q = `?t=${token}`;
|
|
1348
|
-
const native =
|
|
1380
|
+
const native = loadNativeAddon();
|
|
1349
1381
|
if (typeof native.generateServeEntryClient !== 'function') {
|
|
1350
1382
|
throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
|
|
1351
1383
|
}
|
|
@@ -1358,87 +1390,32 @@ function emitEntryClient(eager, lazy, token) {
|
|
|
1358
1390
|
*/
|
|
1359
1391
|
function emitEntryEvent(token) {
|
|
1360
1392
|
const q = `?t=${token}`;
|
|
1361
|
-
const native =
|
|
1393
|
+
const native = loadNativeAddon();
|
|
1362
1394
|
if (typeof native.generateServeEntryEvent !== 'function') {
|
|
1363
1395
|
throw new Error('vmz native addon missing generateServeEntryEvent — rebuild with `pnpm napi:build`');
|
|
1364
1396
|
}
|
|
1365
1397
|
return native.generateServeEntryEvent(q);
|
|
1366
1398
|
}
|
|
1367
|
-
/**
|
|
1368
|
-
* Load vmz N-API CodeGenerators (same discovery as `@vmz/vmz` native-addon).
|
|
1369
|
-
* @returns {any}
|
|
1370
|
-
*/
|
|
1371
|
-
function requireNativeGenerator() {
|
|
1372
|
-
if (_nativeGen !== undefined) {
|
|
1373
|
-
if (!_nativeGen) {
|
|
1374
|
-
throw new Error('vmz native addon missing — run `pnpm napi:build` (serve entry printers live in vmz-generator via N-API)');
|
|
1375
|
-
}
|
|
1376
|
-
return _nativeGen;
|
|
1377
|
-
}
|
|
1378
|
-
try {
|
|
1379
|
-
const envPath = (typeof process.env.VMZ_NATIVE_NODE === 'string' && process.env.VMZ_NATIVE_NODE.trim()) || '';
|
|
1380
|
-
if (envPath) {
|
|
1381
|
-
_nativeGen = require(path.resolve(envPath));
|
|
1382
|
-
return _nativeGen;
|
|
1383
|
-
}
|
|
1384
|
-
const { platform, arch } = process;
|
|
1385
|
-
let triple = `${platform}-${arch}`;
|
|
1386
|
-
if (platform === 'win32' && arch === 'x64')
|
|
1387
|
-
triple = 'win32-x64-msvc';
|
|
1388
|
-
else if (platform === 'win32' && arch === 'arm64')
|
|
1389
|
-
triple = 'win32-arm64-msvc';
|
|
1390
|
-
else if (platform === 'darwin' && arch === 'arm64')
|
|
1391
|
-
triple = 'darwin-arm64';
|
|
1392
|
-
else if (platform === 'darwin' && arch === 'x64')
|
|
1393
|
-
triple = 'darwin-x64';
|
|
1394
|
-
else if (platform === 'linux' && arch === 'x64')
|
|
1395
|
-
triple = 'linux-x64-gnu';
|
|
1396
|
-
else if (platform === 'linux' && arch === 'arm64')
|
|
1397
|
-
triple = 'linux-arm64-gnu';
|
|
1398
|
-
const short = triple === 'win32-x64-msvc'
|
|
1399
|
-
? 'win32-x64'
|
|
1400
|
-
: triple === 'win32-arm64-msvc'
|
|
1401
|
-
? 'win32-arm64'
|
|
1402
|
-
: triple === 'linux-x64-gnu'
|
|
1403
|
-
? 'linux-x64'
|
|
1404
|
-
: triple === 'linux-arm64-gnu'
|
|
1405
|
-
? 'linux-arm64'
|
|
1406
|
-
: triple;
|
|
1407
|
-
const name = `@vmz/vmz-${short}`;
|
|
1408
|
-
/** @type {string[]} */
|
|
1409
|
-
const candidates = [];
|
|
1410
|
-
try {
|
|
1411
|
-
const resolved = require.resolve(`${name}/package.json`);
|
|
1412
|
-
const dir = path.dirname(resolved);
|
|
1413
|
-
candidates.push(path.join(dir, `vmz.${triple}.node`), path.join(dir, 'vmz.node'));
|
|
1414
|
-
}
|
|
1415
|
-
catch {
|
|
1416
|
-
/* optional */
|
|
1417
|
-
}
|
|
1418
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1419
|
-
candidates.push(path.join(here, 'node_modules', name, `vmz.${triple}.node`), path.join(here, 'node_modules', name, 'vmz.node'), path.join(here, '..', 'node_modules', name, `vmz.${triple}.node`), path.join(here, '..', 'node_modules', name, 'vmz.node'));
|
|
1420
|
-
for (const p of candidates) {
|
|
1421
|
-
if (existsSync(p)) {
|
|
1422
|
-
_nativeGen = require(p);
|
|
1423
|
-
return _nativeGen;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
_nativeGen = null;
|
|
1427
|
-
}
|
|
1428
|
-
catch {
|
|
1429
|
-
_nativeGen = null;
|
|
1430
|
-
}
|
|
1431
|
-
if (!_nativeGen) {
|
|
1432
|
-
throw new Error('vmz native addon missing — run `pnpm napi:build` (serve entry printers live in vmz-generator via N-API)');
|
|
1433
|
-
}
|
|
1434
|
-
return _nativeGen;
|
|
1435
|
-
}
|
|
1436
1399
|
/**
|
|
1437
1400
|
* Style Theme cookie / localStorage key (host contract, not a second theme API).
|
|
1438
1401
|
*/
|
|
1439
1402
|
const THEME_STORE_KEY = 'vmz-theme';
|
|
1440
1403
|
/** Host preference key for `routing.strategy: 'none'` (cookie + localStorage). */
|
|
1441
1404
|
const LOCALE_STORE_KEY = 'vmz.locale';
|
|
1405
|
+
/**
|
|
1406
|
+
* Cache-bust stylesheet entry for dev reload (token + serve revision).
|
|
1407
|
+
* @param {string | null | undefined} entry
|
|
1408
|
+
*/
|
|
1409
|
+
function cssEntryWithBust(entry) {
|
|
1410
|
+
if (!entry)
|
|
1411
|
+
return undefined;
|
|
1412
|
+
const base = String(entry).replace(/^\/+/, '');
|
|
1413
|
+
const params = new URLSearchParams();
|
|
1414
|
+
params.set('t', String(reloadToken));
|
|
1415
|
+
if (styleBundleHash)
|
|
1416
|
+
params.set('h', styleBundleHash);
|
|
1417
|
+
return `${base}?${params.toString()}`;
|
|
1418
|
+
}
|
|
1442
1419
|
/**
|
|
1443
1420
|
* @param {string} dir
|
|
1444
1421
|
* @returns {Promise<{ cssEntry: string|null, styleTheme: typeof styleTheme, styleBundleHash: string|null }>}
|
package/dist/server.js
CHANGED
|
@@ -751,9 +751,15 @@ async function sendHtmlStream(res, status, source, signal) {
|
|
|
751
751
|
* @param {string} type
|
|
752
752
|
*/
|
|
753
753
|
function sendBytes(res, status, body, type) {
|
|
754
|
-
|
|
754
|
+
/** @type {Record<string, string | number>} */
|
|
755
|
+
const headers = {
|
|
755
756
|
'content-type': type,
|
|
756
757
|
'content-length': body.byteLength,
|
|
757
|
-
}
|
|
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);
|
|
758
764
|
res.end(body);
|
|
759
765
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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": [
|
|
@@ -53,6 +57,14 @@
|
|
|
53
57
|
"dependencies": {
|
|
54
58
|
"linkedom": "^0.18.13"
|
|
55
59
|
},
|
|
60
|
+
"optionalDependencies": {
|
|
61
|
+
"@vmz/vmz-win32-x64": "0.1.13",
|
|
62
|
+
"@vmz/vmz-win32-arm64": "0.1.13",
|
|
63
|
+
"@vmz/vmz-darwin-x64": "0.1.13",
|
|
64
|
+
"@vmz/vmz-darwin-arm64": "0.1.13",
|
|
65
|
+
"@vmz/vmz-linux-x64": "0.1.13",
|
|
66
|
+
"@vmz/vmz-linux-arm64": "0.1.13"
|
|
67
|
+
},
|
|
56
68
|
"publishConfig": {
|
|
57
69
|
"access": "public"
|
|
58
70
|
},
|