@vmz/vmz 0.0.3 → 0.1.0
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/README.md +6 -4
- package/dist/build-assemble.d.ts +52 -0
- package/dist/build-assemble.js +191 -0
- package/dist/cdn-policy.d.ts +196 -0
- package/dist/cdn-policy.js +443 -0
- package/dist/cli.js +152 -11
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/delivery-profile.d.ts +74 -0
- package/dist/delivery-profile.js +279 -0
- package/dist/dev-session.js +49 -13
- package/dist/document-build.js +9 -4
- package/dist/document-designs.js +30 -2
- package/dist/document-enrich.js +8 -0
- package/dist/embedded-packaging.d.ts +22 -0
- package/dist/embedded-packaging.js +113 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.js +35 -15
- package/dist/invocation.d.ts +8 -31
- package/dist/invocation.js +12 -33
- package/dist/locale-check.d.ts +16 -0
- package/dist/locale-check.js +131 -3
- package/dist/locale-cmd.js +2 -2
- package/dist/locale-route-emit.d.ts +34 -0
- package/dist/locale-route-emit.js +134 -0
- package/dist/locale-router.d.ts +20 -0
- package/dist/locale-router.js +68 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +11 -3
- package/dist/pack.d.ts +40 -0
- package/dist/pack.js +108 -0
- package/dist/plugin-host.d.ts +10 -1
- package/dist/plugin-host.js +19 -2
- package/dist/port.d.ts +10 -0
- package/dist/port.js +46 -0
- package/dist/production-observability.d.ts +286 -0
- package/dist/production-observability.js +469 -0
- package/dist/production-test-pack.d.ts +144 -0
- package/dist/production-test-pack.js +447 -0
- package/dist/release-cmd.d.ts +8 -0
- package/dist/release-cmd.js +126 -0
- package/dist/release-pack.d.ts +96 -0
- package/dist/release-pack.js +346 -0
- package/dist/server-artifact.d.ts +140 -0
- package/dist/server-artifact.js +205 -0
- package/dist/server-language-backend.d.ts +89 -0
- package/dist/server-language-backend.js +121 -0
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +145 -0
- package/dist/static-emit.js +577 -0
- package/package.json +13 -13
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B0 — Delivery profile authoring normalize + CLI --profile resolve.
|
|
3
|
+
* Pure data only; expands legacy site-delivery sugar into profiles[default].
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
export const DELIVERY_PROFILE_AUTHORING_SCHEMA = 'vmz.delivery.authoring.v0';
|
|
8
|
+
export const BUILD_PROFILE_SELECTION_SCHEMA = 'vmz.build.profile_selection.v0';
|
|
9
|
+
/** Browser-era assembly kinds (04 B5). */
|
|
10
|
+
export const ASSEMBLIES = Object.freeze(['local-static', 'static-cdn', 'server-host', 'cdn+server', 'rust-embedded']);
|
|
11
|
+
export const SERVER_RUNTIMES = Object.freeze(['node', 'worker', 'deno', 'bun', 'rust-host']);
|
|
12
|
+
/** Official built-in aliases when not overridden in config. */
|
|
13
|
+
export const BUILTIN_PROFILES = Object.freeze({
|
|
14
|
+
'web-client': { host: 'browser', assembly: 'local-static' },
|
|
15
|
+
'web-static': { host: 'browser', assembly: 'static-cdn' },
|
|
16
|
+
'web-ssr': { host: 'browser', assembly: 'server-host', serverRuntime: 'node' },
|
|
17
|
+
'web-hybrid': { host: 'browser', assembly: 'cdn+server', serverRuntime: 'node' },
|
|
18
|
+
});
|
|
19
|
+
function isPlainObject(v) {
|
|
20
|
+
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
export function pickSiteAuthoring(raw) {
|
|
23
|
+
if (!isPlainObject(raw))
|
|
24
|
+
return null;
|
|
25
|
+
if (!Array.isArray(raw.sources) || raw.sources.length < 1)
|
|
26
|
+
return null;
|
|
27
|
+
if (typeof raw.artifact !== 'string' || !String(raw.artifact).trim())
|
|
28
|
+
return null;
|
|
29
|
+
const site = {
|
|
30
|
+
artifact: String(raw.artifact),
|
|
31
|
+
sources: raw.sources,
|
|
32
|
+
};
|
|
33
|
+
for (const k of [
|
|
34
|
+
'siteId',
|
|
35
|
+
'resolution',
|
|
36
|
+
'activation',
|
|
37
|
+
'expectedCompatibility',
|
|
38
|
+
'failure',
|
|
39
|
+
'failurePolicy',
|
|
40
|
+
'update',
|
|
41
|
+
'updatePolicy',
|
|
42
|
+
'rollback',
|
|
43
|
+
'rollbackPolicy',
|
|
44
|
+
'security',
|
|
45
|
+
'securityPolicy',
|
|
46
|
+
]) {
|
|
47
|
+
if (raw[k] !== undefined)
|
|
48
|
+
site[k] = raw[k];
|
|
49
|
+
}
|
|
50
|
+
return site;
|
|
51
|
+
}
|
|
52
|
+
function normalizeProfileEntry(entry, id, diagnostics) {
|
|
53
|
+
if (!isPlainObject(entry)) {
|
|
54
|
+
diagnostics.push({ code: 'delivery.profile.invalid', message: `profiles.${id} must be an object` });
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const host = String(entry.host || 'browser');
|
|
58
|
+
if (host !== 'browser') {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
code: 'delivery.profile.host',
|
|
61
|
+
message: `profiles.${id}.host: only 'browser' is supported before Browser Production (got ${host})`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const assembly = String(entry.assembly || '').trim();
|
|
65
|
+
if (!ASSEMBLIES.includes(assembly)) {
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
code: 'delivery.profile.assembly',
|
|
68
|
+
message: `profiles.${id}.assembly must be one of ${ASSEMBLIES.join('|')} (got ${assembly || '(empty)'})`,
|
|
69
|
+
});
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let serverRuntime = null;
|
|
73
|
+
if (assembly === 'server-host' || assembly === 'cdn+server') {
|
|
74
|
+
serverRuntime = String(entry.serverRuntime || 'node');
|
|
75
|
+
if (!SERVER_RUNTIMES.includes(serverRuntime)) {
|
|
76
|
+
diagnostics.push({
|
|
77
|
+
code: 'delivery.profile.serverRuntime',
|
|
78
|
+
message: `profiles.${id}.serverRuntime must be one of ${SERVER_RUNTIMES.join('|')}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let sources = null;
|
|
83
|
+
if (entry.sources != null) {
|
|
84
|
+
if (isPlainObject(entry.sources) && Array.isArray(entry.sources.sources)) {
|
|
85
|
+
sources = pickSiteAuthoring(entry.sources);
|
|
86
|
+
}
|
|
87
|
+
else if (Array.isArray(entry.sources)) {
|
|
88
|
+
sources = pickSiteAuthoring({
|
|
89
|
+
artifact: entry.artifact || entry.sourcesArtifact || id,
|
|
90
|
+
sources: entry.sources,
|
|
91
|
+
resolution: entry.resolution,
|
|
92
|
+
activation: entry.activation,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
diagnostics.push({
|
|
97
|
+
code: 'delivery.profile.sources',
|
|
98
|
+
message: `profiles.${id}.sources must be defineSite({...}) or a sources array with artifact`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (entry.sources != null && sources == null) {
|
|
102
|
+
const already = diagnostics.some((d) => String(d.message || '').includes(`profiles.${id}`));
|
|
103
|
+
if (!already) {
|
|
104
|
+
diagnostics.push({
|
|
105
|
+
code: 'delivery.profile.sources.artifact',
|
|
106
|
+
message: `profiles.${id} site sources require artifact string`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
host: 'browser',
|
|
114
|
+
assembly,
|
|
115
|
+
serverRuntime,
|
|
116
|
+
sources,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function normalizeDeliveryAuthoring(raw) {
|
|
120
|
+
const diagnostics = [];
|
|
121
|
+
if (raw == null) {
|
|
122
|
+
const profiles = { ...BUILTIN_PROFILES };
|
|
123
|
+
const normalized = {};
|
|
124
|
+
for (const [id, entry] of Object.entries(profiles)) {
|
|
125
|
+
const n = normalizeProfileEntry(entry, id, diagnostics);
|
|
126
|
+
if (n)
|
|
127
|
+
normalized[id] = n;
|
|
128
|
+
}
|
|
129
|
+
const table = {
|
|
130
|
+
schema: DELIVERY_PROFILE_AUTHORING_SCHEMA,
|
|
131
|
+
default: 'web-ssr',
|
|
132
|
+
profiles: normalized,
|
|
133
|
+
sugar: false,
|
|
134
|
+
};
|
|
135
|
+
table.digest = sha256Hex(canonicalJson(table));
|
|
136
|
+
return { ok: true, table };
|
|
137
|
+
}
|
|
138
|
+
if (!isPlainObject(raw)) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
diagnostics: [{ code: 'delivery.invalid', message: 'delivery must be a plain object' }],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
let profileInputs = {};
|
|
145
|
+
let defaultId = '';
|
|
146
|
+
let sugar = false;
|
|
147
|
+
if (isPlainObject(raw.profiles)) {
|
|
148
|
+
defaultId = String(raw.default || '').trim();
|
|
149
|
+
profileInputs = { ...BUILTIN_PROFILES, ...raw.profiles };
|
|
150
|
+
if (!defaultId) {
|
|
151
|
+
const keys = Object.keys(raw.profiles);
|
|
152
|
+
defaultId = keys[0] || 'web-ssr';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (Array.isArray(raw.sources) || raw.artifact != null || raw.assembly != null) {
|
|
156
|
+
sugar = true;
|
|
157
|
+
const site = pickSiteAuthoring(raw);
|
|
158
|
+
defaultId = String(raw.default || raw.artifact || 'web-ssr').trim() || 'web-ssr';
|
|
159
|
+
const assembly = typeof raw.assembly === 'string' && ASSEMBLIES.includes(raw.assembly) ? raw.assembly : site ? 'rust-embedded' : 'server-host';
|
|
160
|
+
profileInputs = {
|
|
161
|
+
...BUILTIN_PROFILES,
|
|
162
|
+
[defaultId]: {
|
|
163
|
+
host: raw.host || 'browser',
|
|
164
|
+
assembly,
|
|
165
|
+
serverRuntime: raw.serverRuntime || 'node',
|
|
166
|
+
...(site
|
|
167
|
+
? {
|
|
168
|
+
artifact: site.artifact,
|
|
169
|
+
sources: site.sources,
|
|
170
|
+
resolution: site.resolution,
|
|
171
|
+
activation: site.activation,
|
|
172
|
+
expectedCompatibility: site.expectedCompatibility,
|
|
173
|
+
failure: site.failure,
|
|
174
|
+
failurePolicy: site.failurePolicy,
|
|
175
|
+
update: site.update,
|
|
176
|
+
updatePolicy: site.updatePolicy,
|
|
177
|
+
rollback: site.rollback,
|
|
178
|
+
rollbackPolicy: site.rollbackPolicy,
|
|
179
|
+
security: site.security,
|
|
180
|
+
securityPolicy: site.securityPolicy,
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
diagnostics: [
|
|
190
|
+
{
|
|
191
|
+
code: 'delivery.shape',
|
|
192
|
+
message: 'delivery must declare profiles{} or legacy { artifact, sources }',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const profiles = {};
|
|
198
|
+
for (const [id, entry] of Object.entries(profileInputs)) {
|
|
199
|
+
const n = normalizeProfileEntry(entry, id, diagnostics);
|
|
200
|
+
if (n)
|
|
201
|
+
profiles[id] = n;
|
|
202
|
+
}
|
|
203
|
+
if (!profiles[defaultId]) {
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
code: 'delivery.default',
|
|
206
|
+
message: `delivery.default '${defaultId}' is not a known profile`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (diagnostics.length)
|
|
210
|
+
return { ok: false, diagnostics };
|
|
211
|
+
const table = {
|
|
212
|
+
schema: DELIVERY_PROFILE_AUTHORING_SCHEMA,
|
|
213
|
+
default: defaultId,
|
|
214
|
+
profiles,
|
|
215
|
+
sugar,
|
|
216
|
+
};
|
|
217
|
+
table.digest = sha256Hex(canonicalJson(table));
|
|
218
|
+
return { ok: true, table };
|
|
219
|
+
}
|
|
220
|
+
export function selectBuildProfile(table, cliProfile = '') {
|
|
221
|
+
const id = String(cliProfile || '').trim() || table.default;
|
|
222
|
+
const profile = table.profiles[id];
|
|
223
|
+
if (!profile) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
diagnostics: [
|
|
227
|
+
{
|
|
228
|
+
code: 'delivery.profile.unknown',
|
|
229
|
+
message: `unknown build --profile ${id} (known: ${Object.keys(table.profiles).join(', ')})`,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const selection = {
|
|
235
|
+
schema: BUILD_PROFILE_SELECTION_SCHEMA,
|
|
236
|
+
profileId: id,
|
|
237
|
+
host: profile.host,
|
|
238
|
+
assembly: profile.assembly,
|
|
239
|
+
serverRuntime: profile.serverRuntime,
|
|
240
|
+
hasSiteSources: Boolean(profile.sources),
|
|
241
|
+
authoringDigest: table.digest,
|
|
242
|
+
fromCli: Boolean(String(cliProfile || '').trim()),
|
|
243
|
+
};
|
|
244
|
+
selection.digest = sha256Hex(canonicalJson(selection));
|
|
245
|
+
return { ok: true, selection, profile };
|
|
246
|
+
}
|
|
247
|
+
export function semanticIdsForAssembly(assembly) {
|
|
248
|
+
switch (assembly) {
|
|
249
|
+
case 'static-cdn':
|
|
250
|
+
return ['static-delivery', 'asset-graph'];
|
|
251
|
+
case 'server-host':
|
|
252
|
+
return ['server-host', 'asset-graph'];
|
|
253
|
+
case 'cdn+server':
|
|
254
|
+
return ['server-host', 'static-delivery', 'asset-graph'];
|
|
255
|
+
case 'rust-embedded':
|
|
256
|
+
return ['site-fallback', 'asset-graph'];
|
|
257
|
+
case 'local-static':
|
|
258
|
+
return ['asset-graph'];
|
|
259
|
+
default:
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
export function canonicalJson(value) {
|
|
264
|
+
return JSON.stringify(sortKeys(value));
|
|
265
|
+
}
|
|
266
|
+
function sortKeys(value) {
|
|
267
|
+
if (Array.isArray(value))
|
|
268
|
+
return value.map(sortKeys);
|
|
269
|
+
if (value && typeof value === 'object') {
|
|
270
|
+
const out = {};
|
|
271
|
+
for (const k of Object.keys(value).sort())
|
|
272
|
+
out[k] = sortKeys(value[k]);
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
export function sha256Hex(text) {
|
|
278
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
279
|
+
}
|
package/dist/dev-session.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync } from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
|
|
12
12
|
import { createWorkspace } from './index.js';
|
|
13
|
+
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
13
14
|
import { log } from './log.js';
|
|
14
15
|
import { diffFingerprints, fileFingerprintMap } from './watch-diff.js';
|
|
15
16
|
/**
|
|
@@ -48,12 +49,24 @@ export function createDevSession(options) {
|
|
|
48
49
|
ws.updateFiles(changes);
|
|
49
50
|
return ws.build();
|
|
50
51
|
}
|
|
52
|
+
function emitLocales() {
|
|
53
|
+
const localeEmit = emitLocaleRuntimeModules(project, outDir);
|
|
54
|
+
// Always surface locale diagnostics (warnings included) — missing /locales must not be silent.
|
|
55
|
+
log.diagnostics(localeEmit.diagnostics ?? []);
|
|
56
|
+
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
57
|
+
log.error('locale runtime emit failed');
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
51
62
|
function printReport(report, label) {
|
|
52
63
|
const errors = log.diagnostics(report.diagnostics ?? []);
|
|
53
64
|
if (errors) {
|
|
54
65
|
log.error(`${label} failed (${errors} error(s))`);
|
|
55
66
|
return false;
|
|
56
67
|
}
|
|
68
|
+
if (!emitLocales())
|
|
69
|
+
return false;
|
|
57
70
|
const mode = report.full ? 'full' : 'affected';
|
|
58
71
|
const chunks = (report.affectedChunks || []).join(', ') || '(none)';
|
|
59
72
|
log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
|
|
@@ -82,7 +95,8 @@ export function createDevSession(options) {
|
|
|
82
95
|
}
|
|
83
96
|
child = spawnHost({ project, outDir, host, port });
|
|
84
97
|
const docsRoot = path.join(project, 'documents');
|
|
85
|
-
const
|
|
98
|
+
const localesRoot = path.join(project, 'locales');
|
|
99
|
+
const watchRoots = [src].concat(existsSync(docsRoot) ? [docsRoot] : []).concat(existsSync(localesRoot) ? [localesRoot] : []);
|
|
86
100
|
log.info(`dev → http://${host}:${port} (watching ${watchRoots.join(', ')})`);
|
|
87
101
|
/** @type {Map<string, Map<string, string>>} */
|
|
88
102
|
let fingerprints = new Map();
|
|
@@ -100,10 +114,12 @@ export function createDevSession(options) {
|
|
|
100
114
|
if (stopped || signal?.aborted)
|
|
101
115
|
break;
|
|
102
116
|
if (child && child.exitCode != null) {
|
|
103
|
-
|
|
117
|
+
log.warn(`serve-host exited (${child.exitCode}) — respawning…`);
|
|
118
|
+
child = spawnHost({ project, outDir, host, port });
|
|
119
|
+
continue;
|
|
104
120
|
}
|
|
105
|
-
/** @type {{ srcChanged: string[], srcDeleted: string[], docsDirty: boolean }} */
|
|
106
|
-
let batch = { srcChanged: [], srcDeleted: [], docsDirty: false };
|
|
121
|
+
/** @type {{ srcChanged: string[], srcDeleted: string[], docsDirty: boolean, localesDirty: boolean }} */
|
|
122
|
+
let batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
|
|
107
123
|
try {
|
|
108
124
|
// Probe only — keep prior fingerprints until debounce resample
|
|
109
125
|
// (same contract as pre-docs watcher: empty second pass would miss soft reload).
|
|
@@ -115,6 +131,10 @@ export function createDevSession(options) {
|
|
|
115
131
|
batch.srcChanged = diff.changed;
|
|
116
132
|
batch.srcDeleted = diff.deleted;
|
|
117
133
|
}
|
|
134
|
+
else if (root === localesRoot) {
|
|
135
|
+
if (diff.changed.length || diff.deleted.length)
|
|
136
|
+
batch.localesDirty = true;
|
|
137
|
+
}
|
|
118
138
|
else if (diff.changed.length || diff.deleted.length) {
|
|
119
139
|
batch.docsDirty = true;
|
|
120
140
|
}
|
|
@@ -124,11 +144,11 @@ export function createDevSession(options) {
|
|
|
124
144
|
log.warn('watch error:', err);
|
|
125
145
|
continue;
|
|
126
146
|
}
|
|
127
|
-
if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty)
|
|
147
|
+
if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty && !batch.localesDirty)
|
|
128
148
|
continue;
|
|
129
149
|
await sleep(200);
|
|
130
150
|
// Resample against the same prior fingerprints, then commit.
|
|
131
|
-
batch = { srcChanged: [], srcDeleted: [], docsDirty: false };
|
|
151
|
+
batch = { srcChanged: [], srcDeleted: [], docsDirty: false, localesDirty: false };
|
|
132
152
|
for (const root of watchRoots) {
|
|
133
153
|
const prev = fingerprints.get(root) || new Map();
|
|
134
154
|
const next = fileFingerprintMap(root);
|
|
@@ -138,11 +158,15 @@ export function createDevSession(options) {
|
|
|
138
158
|
batch.srcChanged = diff.changed;
|
|
139
159
|
batch.srcDeleted = diff.deleted;
|
|
140
160
|
}
|
|
161
|
+
else if (root === localesRoot) {
|
|
162
|
+
if (diff.changed.length || diff.deleted.length)
|
|
163
|
+
batch.localesDirty = true;
|
|
164
|
+
}
|
|
141
165
|
else if (diff.changed.length || diff.deleted.length) {
|
|
142
166
|
batch.docsDirty = true;
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
|
-
if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty)
|
|
169
|
+
if (!batch.srcChanged.length && !batch.srcDeleted.length && !batch.docsDirty && !batch.localesDirty)
|
|
146
170
|
continue;
|
|
147
171
|
let needFullReload = batch.docsDirty;
|
|
148
172
|
if (batch.srcChanged.length || batch.srcDeleted.length) {
|
|
@@ -180,12 +204,26 @@ export function createDevSession(options) {
|
|
|
180
204
|
: 'soft reload ok (full page)');
|
|
181
205
|
}
|
|
182
206
|
catch (err) {
|
|
183
|
-
log.warn(`soft reload failed (${err}) —
|
|
184
|
-
killChild(child);
|
|
185
|
-
child = spawnHost({ project, outDir, host, port });
|
|
207
|
+
log.warn(`soft reload failed (${err}) — keeping previous serve-host (fix and save)`);
|
|
186
208
|
}
|
|
187
209
|
continue;
|
|
188
210
|
}
|
|
211
|
+
if (batch.localesDirty) {
|
|
212
|
+
log.info('locales change detected — re-emitting #locales runtime…');
|
|
213
|
+
if (!emitLocales()) {
|
|
214
|
+
log.warn('locale runtime emit failed — keeping previous modules');
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await softReload(host, port, { full: true, islandHmr: false });
|
|
219
|
+
log.info('soft reload ok (full page; locales)');
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log.warn(`soft reload failed (${err}) — keeping previous serve-host (fix and save)`);
|
|
223
|
+
}
|
|
224
|
+
if (!batch.docsDirty)
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
189
227
|
if (batch.docsDirty) {
|
|
190
228
|
log.info('documents change detected — rebuilding document mount…');
|
|
191
229
|
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
@@ -198,9 +236,7 @@ export function createDevSession(options) {
|
|
|
198
236
|
log.info('soft reload ok (full page; docs)');
|
|
199
237
|
}
|
|
200
238
|
catch (err) {
|
|
201
|
-
log.warn(`soft reload failed (${err}) —
|
|
202
|
-
killChild(child);
|
|
203
|
-
child = spawnHost({ project, outDir, host, port });
|
|
239
|
+
log.warn(`soft reload failed (${err}) — keeping previous serve-host (fix and save)`);
|
|
204
240
|
}
|
|
205
241
|
}
|
|
206
242
|
}
|
package/dist/document-build.js
CHANGED
|
@@ -164,11 +164,12 @@ function renderStaticHtml({ title, locale, route, nav, bodyHtml, headings, desig
|
|
|
164
164
|
/** @type {string[]} */
|
|
165
165
|
const cssHrefs = [];
|
|
166
166
|
if (hostChrome) {
|
|
167
|
-
//
|
|
168
|
-
|
|
167
|
+
// Integrated documents are served with pretty directory URLs. Root
|
|
168
|
+
// absolute assets remain correct for both emitted files and rewrites.
|
|
169
|
+
cssHrefs.push('/vmz.css');
|
|
169
170
|
}
|
|
170
171
|
if (designsHref)
|
|
171
|
-
cssHrefs.push(prefix + designsHref);
|
|
172
|
+
cssHrefs.push(hostChrome ? `/${designsHref}` : prefix + designsHref);
|
|
172
173
|
const cssLink = cssHrefs.map((href) => ` <link rel="stylesheet" href="${esc(href)}" />`).join('\n') + (cssHrefs.length ? '\n' : '');
|
|
173
174
|
const navItems = nav
|
|
174
175
|
.map((n) => {
|
|
@@ -200,13 +201,17 @@ ${cssLink}</head>
|
|
|
200
201
|
<div class="site site--docs">
|
|
201
202
|
<a class="skip-link" href="#main">Skip to content</a>
|
|
202
203
|
${header}
|
|
204
|
+
<div class="doc-body">
|
|
205
|
+
<aside class="doc-sidebar">
|
|
203
206
|
${docsNav}
|
|
204
207
|
${searchShellHtml}
|
|
205
|
-
|
|
208
|
+
</aside>
|
|
209
|
+
<div class="doc-content">
|
|
206
210
|
${toc}<main id="main">
|
|
207
211
|
${bodyHtml}
|
|
208
212
|
${playgroundShellHtml}
|
|
209
213
|
</main>
|
|
214
|
+
</div>
|
|
210
215
|
</div>
|
|
211
216
|
${hostChrome.footer}
|
|
212
217
|
</div>
|
package/dist/document-designs.js
CHANGED
|
@@ -64,12 +64,17 @@ function emitMinimalDesignsCss(designsDir) {
|
|
|
64
64
|
const vars = {};
|
|
65
65
|
const tokenDir = path.join(designsDir, 'tokens');
|
|
66
66
|
if (fs.existsSync(tokenDir)) {
|
|
67
|
-
walkJson(tokenDir, (obj, prefix) =>
|
|
67
|
+
walkJson(tokenDir, (obj, prefix) => {
|
|
68
|
+
collectStyleThemeEntries(obj, vars);
|
|
69
|
+
flattenTokens(obj, prefix, vars);
|
|
70
|
+
});
|
|
68
71
|
}
|
|
69
72
|
const themeJson = path.join(designsDir, 'theme.json');
|
|
70
73
|
if (fs.existsSync(themeJson)) {
|
|
71
74
|
try {
|
|
72
|
-
|
|
75
|
+
const theme = JSON.parse(fs.readFileSync(themeJson, 'utf8'));
|
|
76
|
+
collectStyleThemeEntries(theme, vars);
|
|
77
|
+
flattenTokens(theme, '', vars);
|
|
73
78
|
}
|
|
74
79
|
catch {
|
|
75
80
|
/* ignore */
|
|
@@ -89,10 +94,33 @@ function cssVar(key) {
|
|
|
89
94
|
.replace(/^-|-$/g, '');
|
|
90
95
|
return `--${name}`;
|
|
91
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Style Theme entries (`key.path` + `value`) → `vmz-*` CSS vars (same naming as application compile).
|
|
99
|
+
* @param {unknown} obj
|
|
100
|
+
* @param {Record<string, string>} out
|
|
101
|
+
*/
|
|
102
|
+
function collectStyleThemeEntries(obj, out) {
|
|
103
|
+
if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
|
|
104
|
+
return;
|
|
105
|
+
const entries = /** @type {{ key?: { path?: unknown }, value?: unknown }[]} */ ( /** @type {{ entries?: unknown }} */(obj).entries);
|
|
106
|
+
if (!Array.isArray(entries))
|
|
107
|
+
return;
|
|
108
|
+
for (const e of entries) {
|
|
109
|
+
const pathParts = e?.key?.path;
|
|
110
|
+
if (!Array.isArray(pathParts) || pathParts.length === 0)
|
|
111
|
+
continue;
|
|
112
|
+
if (typeof e.value !== 'string' && typeof e.value !== 'number')
|
|
113
|
+
continue;
|
|
114
|
+
const dotted = pathParts.map(String).join('-');
|
|
115
|
+
out[`vmz-${dotted}`] = String(e.value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
92
118
|
function flattenTokens(obj, prefix, out) {
|
|
93
119
|
if (obj == null || typeof obj !== 'object' || Array.isArray(obj))
|
|
94
120
|
return;
|
|
95
121
|
for (const [k, v] of Object.entries(obj)) {
|
|
122
|
+
if (k === 'entries')
|
|
123
|
+
continue;
|
|
96
124
|
const key = prefix ? `${prefix}-${k}` : k;
|
|
97
125
|
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
98
126
|
if ('value' in v && (typeof v.value === 'string' || typeof v.value === 'number')) {
|
package/dist/document-enrich.js
CHANGED
|
@@ -199,6 +199,14 @@ function resolveDocHref(href, fromPageKey, locale, routeBase, pageKeySet) {
|
|
|
199
199
|
pk = normalizePageKey(pk);
|
|
200
200
|
const keys = pageKeySet.get(locale) || new Set();
|
|
201
201
|
if (!keys.has(pk)) {
|
|
202
|
+
// A directory index and a leaf page share the same normalized PageKey shape.
|
|
203
|
+
// Prefer the regular sibling resolution above, then retry relative to the
|
|
204
|
+
// PageKey itself so `guide/optimizations/index.md` keeps its directory.
|
|
205
|
+
const indexJoined = path.posix.normalize(path.posix.join(fromPageKey || '.', pathPart));
|
|
206
|
+
const indexPk = normalizePageKey(indexJoined.replace(/^\.\//, ''));
|
|
207
|
+
if (keys.has(indexPk)) {
|
|
208
|
+
return { ok: true, locale, pageKey: indexPk, anchor, anchors: [] };
|
|
209
|
+
}
|
|
202
210
|
return { ok: false, reason: `no PageKey ${pk} in ${locale}` };
|
|
203
211
|
}
|
|
204
212
|
return { ok: true, locale, pageKey: pk, anchor, anchors: [] };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rust-embedded packaging adapter: resource index + baseline closure.
|
|
3
|
+
* Packaging only — does not invent route / MIME / fallback semantics.
|
|
4
|
+
*/
|
|
5
|
+
export declare const EMBEDDED_RESOURCE_INDEX_SCHEMA = "vmz.embedded.resource_index.v0";
|
|
6
|
+
/**
|
|
7
|
+
* Walk outDir and build path → digest → relative blob path map.
|
|
8
|
+
* Copies files into `dist/_vmz/embedded-baseline/` (whole release, no file-level mix).
|
|
9
|
+
* @param {string} outDir
|
|
10
|
+
* @param {{ siteId?: string, contractDigest?: string | null }} [opts]
|
|
11
|
+
*/
|
|
12
|
+
export declare function emitEmbeddedPackaging(outDir: any, opts?: {}): {
|
|
13
|
+
index: {
|
|
14
|
+
schema: string;
|
|
15
|
+
siteId: any;
|
|
16
|
+
contractDigest: any;
|
|
17
|
+
objectCount: number;
|
|
18
|
+
objects: any[];
|
|
19
|
+
};
|
|
20
|
+
indexPath: string;
|
|
21
|
+
baselineDir: string;
|
|
22
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rust-embedded packaging adapter: resource index + baseline closure.
|
|
3
|
+
* Packaging only — does not invent route / MIME / fallback semantics.
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
export const EMBEDDED_RESOURCE_INDEX_SCHEMA = 'vmz.embedded.resource_index.v0';
|
|
10
|
+
/** Paths that must never enter the embedded baseline closure. */
|
|
11
|
+
const SKIP_NAMES = new Set([
|
|
12
|
+
'vmz-serve-host.mjs',
|
|
13
|
+
'vmz-serve-host.js',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'.git',
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Walk outDir and build path → digest → relative blob path map.
|
|
19
|
+
* Copies files into `dist/_vmz/embedded-baseline/` (whole release, no file-level mix).
|
|
20
|
+
* @param {string} outDir
|
|
21
|
+
* @param {{ siteId?: string, contractDigest?: string | null }} [opts]
|
|
22
|
+
*/
|
|
23
|
+
export function emitEmbeddedPackaging(outDir, opts = {}) {
|
|
24
|
+
const vmzDir = path.join(outDir, '_vmz');
|
|
25
|
+
const baselineDir = path.join(vmzDir, 'embedded-baseline');
|
|
26
|
+
fs.mkdirSync(baselineDir, { recursive: true });
|
|
27
|
+
/** @type {Array<{ path: string, digest: string, blob: string, bytes: number }>} */
|
|
28
|
+
const objects = [];
|
|
29
|
+
const root = path.resolve(outDir);
|
|
30
|
+
walkFiles(root, (abs) => {
|
|
31
|
+
const rel = toPosix(path.relative(root, abs));
|
|
32
|
+
if (!rel || rel.startsWith('_vmz/embedded-baseline'))
|
|
33
|
+
return;
|
|
34
|
+
if (rel.startsWith('_vmz/embedded-resource-index'))
|
|
35
|
+
return;
|
|
36
|
+
const base = path.basename(abs);
|
|
37
|
+
if (SKIP_NAMES.has(base))
|
|
38
|
+
return;
|
|
39
|
+
// Keep other _vmz manifests inside baseline (contract, capability table, etc.)
|
|
40
|
+
const buf = fs.readFileSync(abs);
|
|
41
|
+
const digest = sha256Hex(buf);
|
|
42
|
+
const blobRel = `objects/${digest.slice(0, 2)}/${digest}`;
|
|
43
|
+
const blobAbs = path.join(baselineDir, blobRel);
|
|
44
|
+
fs.mkdirSync(path.dirname(blobAbs), { recursive: true });
|
|
45
|
+
if (!fs.existsSync(blobAbs))
|
|
46
|
+
fs.writeFileSync(blobAbs, buf);
|
|
47
|
+
// Also mirror tree under baseline/tree for host convenience
|
|
48
|
+
const treeAbs = path.join(baselineDir, 'tree', rel);
|
|
49
|
+
fs.mkdirSync(path.dirname(treeAbs), { recursive: true });
|
|
50
|
+
fs.copyFileSync(abs, treeAbs);
|
|
51
|
+
objects.push({ path: rel, digest, blob: blobRel, bytes: buf.length });
|
|
52
|
+
});
|
|
53
|
+
objects.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
54
|
+
const index = {
|
|
55
|
+
schema: EMBEDDED_RESOURCE_INDEX_SCHEMA,
|
|
56
|
+
siteId: opts.siteId || null,
|
|
57
|
+
contractDigest: opts.contractDigest || null,
|
|
58
|
+
objectCount: objects.length,
|
|
59
|
+
objects,
|
|
60
|
+
};
|
|
61
|
+
index.indexDigest = sha256Hex(canonicalJson({ ...index, indexDigest: undefined }));
|
|
62
|
+
const indexPath = path.join(vmzDir, 'embedded-resource-index.json');
|
|
63
|
+
fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
|
|
64
|
+
// Optional include_bytes entry point for Rust packaging adapters
|
|
65
|
+
const rsPath = path.join(vmzDir, 'embedded_site.rs');
|
|
66
|
+
fs.writeFileSync(rsPath, `// @generated by vmz — rust-embedded packaging adapter (do not edit)
|
|
67
|
+
// Index digest: ${index.indexDigest}
|
|
68
|
+
pub const EMBEDDED_RESOURCE_INDEX: &str = include_str!("embedded-resource-index.json");
|
|
69
|
+
`, 'utf8');
|
|
70
|
+
return { index, indexPath, baselineDir };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} dir
|
|
74
|
+
* @param {(abs: string) => void} onFile
|
|
75
|
+
*/
|
|
76
|
+
function walkFiles(dir, onFile) {
|
|
77
|
+
if (!fs.existsSync(dir))
|
|
78
|
+
return;
|
|
79
|
+
for (const name of fs.readdirSync(dir)) {
|
|
80
|
+
if (SKIP_NAMES.has(name))
|
|
81
|
+
continue;
|
|
82
|
+
const abs = path.join(dir, name);
|
|
83
|
+
const st = fs.statSync(abs);
|
|
84
|
+
if (st.isDirectory()) {
|
|
85
|
+
if (name === 'embedded-baseline' && path.basename(path.dirname(abs)) === '_vmz')
|
|
86
|
+
continue;
|
|
87
|
+
walkFiles(abs, onFile);
|
|
88
|
+
}
|
|
89
|
+
else if (st.isFile()) {
|
|
90
|
+
onFile(abs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function toPosix(p) {
|
|
95
|
+
return p.split(path.sep).join('/');
|
|
96
|
+
}
|
|
97
|
+
function sha256Hex(data) {
|
|
98
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
99
|
+
}
|
|
100
|
+
function canonicalJson(value) {
|
|
101
|
+
return JSON.stringify(sortKeys(value));
|
|
102
|
+
}
|
|
103
|
+
function sortKeys(value) {
|
|
104
|
+
if (Array.isArray(value))
|
|
105
|
+
return value.map(sortKeys);
|
|
106
|
+
if (value && typeof value === 'object') {
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const k of Object.keys(value).sort())
|
|
109
|
+
out[k] = sortKeys(value[k]);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|