@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.
Files changed (52) hide show
  1. package/README.md +6 -4
  2. package/dist/build-assemble.d.ts +52 -0
  3. package/dist/build-assemble.js +191 -0
  4. package/dist/cdn-policy.d.ts +196 -0
  5. package/dist/cdn-policy.js +443 -0
  6. package/dist/cli.js +152 -11
  7. package/dist/content-addressed-assets.d.ts +69 -0
  8. package/dist/content-addressed-assets.js +206 -0
  9. package/dist/delivery-profile.d.ts +74 -0
  10. package/dist/delivery-profile.js +279 -0
  11. package/dist/dev-session.js +49 -13
  12. package/dist/document-build.js +9 -4
  13. package/dist/document-designs.js +30 -2
  14. package/dist/document-enrich.js +8 -0
  15. package/dist/embedded-packaging.d.ts +22 -0
  16. package/dist/embedded-packaging.js +113 -0
  17. package/dist/index.d.ts +19 -3
  18. package/dist/index.js +35 -15
  19. package/dist/invocation.d.ts +8 -31
  20. package/dist/invocation.js +12 -33
  21. package/dist/locale-check.d.ts +16 -0
  22. package/dist/locale-check.js +131 -3
  23. package/dist/locale-cmd.js +2 -2
  24. package/dist/locale-route-emit.d.ts +34 -0
  25. package/dist/locale-route-emit.js +134 -0
  26. package/dist/locale-router.d.ts +20 -0
  27. package/dist/locale-router.js +68 -0
  28. package/dist/log.d.ts +2 -2
  29. package/dist/log.js +11 -3
  30. package/dist/pack.d.ts +40 -0
  31. package/dist/pack.js +108 -0
  32. package/dist/plugin-host.d.ts +10 -1
  33. package/dist/plugin-host.js +19 -2
  34. package/dist/port.d.ts +10 -0
  35. package/dist/port.js +46 -0
  36. package/dist/production-observability.d.ts +286 -0
  37. package/dist/production-observability.js +469 -0
  38. package/dist/production-test-pack.d.ts +144 -0
  39. package/dist/production-test-pack.js +447 -0
  40. package/dist/release-cmd.d.ts +8 -0
  41. package/dist/release-cmd.js +126 -0
  42. package/dist/release-pack.d.ts +96 -0
  43. package/dist/release-pack.js +346 -0
  44. package/dist/server-artifact.d.ts +140 -0
  45. package/dist/server-artifact.js +205 -0
  46. package/dist/server-language-backend.d.ts +89 -0
  47. package/dist/server-language-backend.js +121 -0
  48. package/dist/site-delivery.d.ts +134 -0
  49. package/dist/site-delivery.js +345 -0
  50. package/dist/static-emit.d.ts +145 -0
  51. package/dist/static-emit.js +577 -0
  52. 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
+ }
@@ -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 watchRoots = [src].concat(existsSync(docsRoot) ? [docsRoot] : []);
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
- throw new Error(`vmz serve-host exited: ${child.exitCode}`);
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}) — restarting serve-host…`);
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}) — restarting serve-host…`);
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
  }
@@ -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
- // Same application stylesheet as landing pages (header/footer chrome).
168
- cssHrefs.push(`${prefix}vmz.css`);
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
- <div class="doc-body">
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>
@@ -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) => flattenTokens(obj, prefix, vars));
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
- flattenTokens(JSON.parse(fs.readFileSync(themeJson, 'utf8')), '', vars);
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')) {
@@ -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
+ }