@vmz/vmz 0.1.13 → 0.1.15

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 (56) hide show
  1. package/dist/author-input.d.ts +19 -0
  2. package/dist/author-input.js +52 -0
  3. package/dist/build-assemble.js +18 -7
  4. package/dist/cdn-policy.js +5 -4
  5. package/dist/cli.js +106 -38
  6. package/dist/content-addressed-assets.d.ts +23 -2
  7. package/dist/content-addressed-assets.js +178 -32
  8. package/dist/delivery-profile.d.ts +30 -2
  9. package/dist/delivery-profile.js +139 -5
  10. package/dist/dev-session.d.ts +1 -0
  11. package/dist/dev-session.js +269 -65
  12. package/dist/dev-watch-roots.d.ts +80 -0
  13. package/dist/dev-watch-roots.js +245 -0
  14. package/dist/document-build.d.ts +4 -3
  15. package/dist/document-build.js +104 -82
  16. package/dist/document-check.d.ts +24 -5
  17. package/dist/document-check.js +64 -101
  18. package/dist/document-cmd.js +2 -9
  19. package/dist/document-enrich.d.ts +2 -1
  20. package/dist/document-enrich.js +15 -4
  21. package/dist/document-integrate.js +29 -21
  22. package/dist/document-layout-render.d.ts +20 -0
  23. package/dist/document-layout-render.js +87 -0
  24. package/dist/document-routing-config.d.ts +17 -0
  25. package/dist/document-routing-config.js +31 -0
  26. package/dist/document-schema.js +8 -1
  27. package/dist/embedded-packaging.js +2 -1
  28. package/dist/index.d.ts +18 -2
  29. package/dist/index.js +63 -4
  30. package/dist/locale-check.js +81 -183
  31. package/dist/locale-cmd.js +13 -45
  32. package/dist/locale-route-emit.d.ts +4 -1
  33. package/dist/locale-route-emit.js +7 -32
  34. package/dist/locale-router.js +26 -8
  35. package/dist/native-addon.d.ts +9 -0
  36. package/dist/native-addon.js +84 -0
  37. package/dist/pack.js +3 -2
  38. package/dist/pretty-json.d.ts +19 -0
  39. package/dist/pretty-json.js +43 -0
  40. package/dist/production-observability.js +3 -2
  41. package/dist/production-test-pack.js +4 -3
  42. package/dist/public-static-assets.d.ts +24 -0
  43. package/dist/public-static-assets.js +140 -0
  44. package/dist/release-pack.js +5 -18
  45. package/dist/route-path.d.ts +35 -0
  46. package/dist/route-path.js +77 -0
  47. package/dist/server-artifact.js +4 -3
  48. package/dist/site-delivery.js +4 -3
  49. package/dist/site-favicon.d.ts +31 -0
  50. package/dist/site-favicon.js +140 -0
  51. package/dist/static-emit.d.ts +21 -0
  52. package/dist/static-emit.js +134 -97
  53. package/dist/test-cmd.js +2 -1
  54. package/dist/wechat-packaging.d.ts +22 -0
  55. package/dist/wechat-packaging.js +59 -0
  56. package/package.json +13 -14
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * A3: content-addressed assets/<hash> layout for immutable CDN objects.
3
- * Logical paths stay available for serve/dev; web-static HTML rewrites to hashed URLs.
4
- * Identical bytes identical asset path (cross-release / cross-source reuse by digest).
3
+ * Logical paths stay available for serve/dev; static HTML rewrites to hashed URLs.
4
+ * CSS aggregators (vmz.css) rewrite `@import` to hashed sibling paths under assets/.
5
+ * JS under assets/ always rewrites ESM `./` → `../` so barrels (vmz-dom → dom-core)
6
+ * resolve at dist root — never prefer hashed siblings for JS (second-hop 404).
5
7
  */
6
8
  // @ts-nocheck
7
9
  import crypto from 'node:crypto';
8
10
  import fs from 'node:fs';
9
11
  import path from 'node:path';
10
12
  import { canonicalJson, sha256Hex } from './release-pack.js';
13
+ import { writePrettyJsonFile } from './pretty-json.js';
11
14
  export const CONTENT_ADDRESSED_ASSETS_SCHEMA = 'vmz.content_addressed_assets.v0';
12
15
  /** Immutable delivery candidates (client-facing bytes). */
13
16
  const DEFAULT_CANDIDATES = [
@@ -15,11 +18,64 @@ const DEFAULT_CANDIDATES = [
15
18
  'entry-event.js',
16
19
  'vmz.css',
17
20
  'vmz-designs.css',
21
+ 'vmz-style.css',
18
22
  'vmz-dom.js',
19
23
  'vmz-runtime.js',
20
24
  'vmz-http.js',
21
25
  'vmz-client-nav.js',
22
26
  ];
27
+ /** CSS files that may @import other logical CSS; processed after leaf CSS is hashed. */
28
+ const CSS_AGGREGATORS = new Set(['vmz.css']);
29
+ /** JS entry shells hashed under assets/; relative ESM must be rewritten first. */
30
+ const JS_ENTRY_AGGREGATORS = new Set(['entry-client.js', 'entry-event.js']);
31
+ const CSS_IMPORT_RE = /@import\s*(?:url\()?['"]?(\.\/)?([^'")\s;]+)['"]?\)?/gi;
32
+ /**
33
+ * Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
34
+ * @param {string} cssText
35
+ * @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
36
+ */
37
+ export function rewriteCssImports(cssText, rewrites) {
38
+ return cssText.replace(CSS_IMPORT_RE, (match, _dot, target) => {
39
+ const logical = String(target || '').replace(/^\.\//, '');
40
+ if (!logical)
41
+ return match;
42
+ const hashed = rewrites[logical] || rewrites[`/${logical}`] || rewrites[`assets/${logical}`];
43
+ if (!hashed)
44
+ return match;
45
+ const rel = hashed.startsWith('/') ? hashed.slice(1) : hashed;
46
+ const sibling = rel.startsWith('assets/') ? `./${path.basename(rel)}` : `./${rel}`;
47
+ return `@import"${sibling}"`;
48
+ });
49
+ }
50
+ /**
51
+ * Rewrite relative ESM so a file served from `/assets/<hash>.js` resolves against
52
+ * dist root (static `from "./x"` / `export * from "./x"` + dynamic `import("./"+…)`).
53
+ *
54
+ * Always use `../…` (Fix A). Do **not** prefer hashed siblings under `assets/`:
55
+ * barrels like `vmz-dom.js` (`export * from './dom-core.js'`) would then resolve
56
+ * as `/assets/dom-core.js` and 404. `rewrites` is accepted for API parity with CSS
57
+ * but intentionally ignored for JS path choice.
58
+ *
59
+ * @param {string} jsText
60
+ * @param {Record<string, string>} [_rewrites]
61
+ */
62
+ export function rewriteJsEntryRelativeImports(jsText, _rewrites = {}) {
63
+ let out = String(jsText || '');
64
+ // Dynamic: import("./" + id) → import("../" + id)
65
+ out = out.replace(/import\(\s*"\.\/"\s*\+/g, 'import("../"+');
66
+ out = out.replace(/import\(\s*'\.\/'\s*\+/g, "import('../'+");
67
+ const rewriteSpec = (spec) => {
68
+ const logical = String(spec || '').replace(/^\.\//, '');
69
+ if (!logical || logical.startsWith('../') || logical.startsWith('/'))
70
+ return spec;
71
+ return `../${logical}`;
72
+ };
73
+ // import/export … from "./x" | import("./x")
74
+ out = out.replace(/\b((?:import|export)\s+[^'"\n]*?\s+from\s+|import\s*\(\s*)(['"])(\.\/[^'"]+)\2/g, (match, prefix, quote, spec) => `${prefix}${quote}${rewriteSpec(spec)}${quote}`);
75
+ // side-effect: import "./x.js"
76
+ out = out.replace(/(^|[;\s])(import\s*)(['"])(\.\/[^'"]+)\3/gm, (match, lead, kw, quote, spec) => `${lead}${kw}${quote}${rewriteSpec(spec)}${quote}`);
77
+ return out;
78
+ }
23
79
  /**
24
80
  * Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
25
81
  * @param {string} distDir
@@ -37,36 +93,55 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
37
93
  const objects = [];
38
94
  /** @type {Record<string, string>} */
39
95
  const rewrites = {};
40
- for (const rel of candidates) {
41
- const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
42
- const src = path.join(abs, ...logical.split('/'));
43
- if (!fs.existsSync(src) || !fs.statSync(src).isFile())
96
+ const ordered = orderCandidates(candidates);
97
+ for (const rel of ordered) {
98
+ // Aggregators are replaced in later passes; leaf JS must rewrite ./ → ../ so a
99
+ // hashed barrel under assets/ never 404s on second-hop relative re-exports.
100
+ if (CSS_AGGREGATORS.has(rel) || JS_ENTRY_AGGREGATORS.has(rel)) {
101
+ ingestCandidate(abs, rel, rewrites, objects, { transform: null });
44
102
  continue;
45
- const buf = fs.readFileSync(src);
46
- const digest = sha256Hex(buf);
47
- const ext = path.extname(logical) || '';
48
- const assetRel = `assets/${digest}${ext}`;
49
- const dest = path.join(abs, ...assetRel.split('/'));
50
- if (!fs.existsSync(dest)) {
51
- fs.mkdirSync(path.dirname(dest), { recursive: true });
52
- fs.writeFileSync(dest, buf);
53
103
  }
54
- else {
55
- // Cross-release reuse: identical digest must not be rewritten.
56
- const existing = sha256Hex(fs.readFileSync(dest));
57
- if (existing !== digest) {
58
- throw new Error(`content-address collision at ${assetRel}`);
59
- }
104
+ if (/\.m?js$/i.test(rel)) {
105
+ const src = path.join(abs, rel);
106
+ if (!fs.existsSync(src))
107
+ continue;
108
+ const rewritten = rewriteJsEntryRelativeImports(fs.readFileSync(src, 'utf8'), {});
109
+ ingestCandidate(abs, rel, rewrites, objects, {
110
+ transform: () => Buffer.from(rewritten, 'utf8'),
111
+ });
112
+ continue;
60
113
  }
61
- objects.push({
62
- logicalPath: logical,
63
- assetPath: assetRel,
64
- digest,
65
- bytes: buf.length,
66
- immutable: true,
114
+ ingestCandidate(abs, rel, rewrites, objects, { transform: null });
115
+ }
116
+ // Aggregator CSS (vmz.css) must import hashed leaf files — rewrite then hash.
117
+ for (const rel of ordered) {
118
+ if (!CSS_AGGREGATORS.has(rel))
119
+ continue;
120
+ const src = path.join(abs, rel);
121
+ if (!fs.existsSync(src))
122
+ continue;
123
+ const rewritten = rewriteCssImports(fs.readFileSync(src, 'utf8'), rewrites);
124
+ removeLogicalObject(objects, rel);
125
+ delete rewrites[`/${rel}`];
126
+ delete rewrites[rel];
127
+ ingestCandidate(abs, rel, rewrites, objects, {
128
+ transform: () => Buffer.from(rewritten, 'utf8'),
129
+ });
130
+ }
131
+ // JS entry shells: rewrite relative ESM (static + dynamic) then re-hash.
132
+ for (const rel of ordered) {
133
+ if (!JS_ENTRY_AGGREGATORS.has(rel))
134
+ continue;
135
+ const src = path.join(abs, rel);
136
+ if (!fs.existsSync(src))
137
+ continue;
138
+ const rewritten = rewriteJsEntryRelativeImports(fs.readFileSync(src, 'utf8'), rewrites);
139
+ removeLogicalObject(objects, rel);
140
+ delete rewrites[`/${rel}`];
141
+ delete rewrites[rel];
142
+ ingestCandidate(abs, rel, rewrites, objects, {
143
+ transform: () => Buffer.from(rewritten, 'utf8'),
67
144
  });
68
- rewrites[`/${logical}`] = `/${assetRel}`;
69
- rewrites[logical] = assetRel;
70
145
  }
71
146
  objects.sort((a, b) => (a.logicalPath < b.logicalPath ? -1 : a.logicalPath > b.logicalPath ? 1 : 0));
72
147
  let rewrittenHtml = 0;
@@ -85,9 +160,83 @@ export function emitContentAddressedAssets(distDir, opts = {}) {
85
160
  const vmzDir = path.join(abs, '_vmz');
86
161
  fs.mkdirSync(vmzDir, { recursive: true });
87
162
  const outPath = path.join(vmzDir, 'content-addressed-assets.json');
88
- fs.writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
163
+ writePrettyJsonFile(outPath, manifest);
89
164
  return { manifest, assetsDir, rewrites, manifestPath: outPath };
90
165
  }
166
+ /**
167
+ * @param {string[]} candidates
168
+ */
169
+ function orderCandidates(candidates) {
170
+ const set = new Set(candidates.map((c) => String(c).replace(/\\/g, '/').replace(/^\//, '')));
171
+ /** @type {string[]} */
172
+ const out = [];
173
+ for (const name of DEFAULT_CANDIDATES) {
174
+ if (set.has(name) && !CSS_AGGREGATORS.has(name) && !JS_ENTRY_AGGREGATORS.has(name))
175
+ out.push(name);
176
+ }
177
+ for (const name of [...set].sort()) {
178
+ if (!CSS_AGGREGATORS.has(name) && !JS_ENTRY_AGGREGATORS.has(name) && !out.includes(name)) {
179
+ out.push(name);
180
+ }
181
+ }
182
+ if (set.has('vmz.css'))
183
+ out.push('vmz.css');
184
+ for (const name of ['entry-client.js', 'entry-event.js']) {
185
+ if (set.has(name))
186
+ out.push(name);
187
+ }
188
+ return out;
189
+ }
190
+ /**
191
+ * @param {Array<Record<string, any>>} objects
192
+ * @param {string} logical
193
+ */
194
+ function removeLogicalObject(objects, logical) {
195
+ const idx = objects.findIndex((o) => o.logicalPath === logical);
196
+ if (idx >= 0)
197
+ objects.splice(idx, 1);
198
+ }
199
+ /**
200
+ * @param {string} absDist
201
+ * @param {string} rel
202
+ * @param {Record<string, string>} rewrites
203
+ * @param {Array<Record<string, any>>} objects
204
+ * @param {{ transform?: ((buf: Buffer) => Buffer) | null }} opts
205
+ */
206
+ function ingestCandidate(absDist, rel, rewrites, objects, opts) {
207
+ const logical = String(rel).replace(/\\/g, '/').replace(/^\//, '');
208
+ const src = path.join(absDist, ...logical.split('/'));
209
+ if (!fs.existsSync(src) || !fs.statSync(src).isFile())
210
+ return;
211
+ let buf = fs.readFileSync(src);
212
+ if (typeof opts.transform === 'function') {
213
+ buf = opts.transform(buf);
214
+ }
215
+ const digest = sha256Hex(buf);
216
+ const ext = path.extname(logical) || '';
217
+ const assetRel = `assets/${digest}${ext}`;
218
+ const dest = path.join(absDist, ...assetRel.split('/'));
219
+ if (!fs.existsSync(dest)) {
220
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
221
+ fs.writeFileSync(dest, buf);
222
+ }
223
+ else {
224
+ const existing = sha256Hex(fs.readFileSync(dest));
225
+ if (existing !== digest) {
226
+ // Stale assets/ from a prior partial build can reuse hash filenames with different bytes.
227
+ fs.writeFileSync(dest, buf);
228
+ }
229
+ }
230
+ objects.push({
231
+ logicalPath: logical,
232
+ assetPath: assetRel,
233
+ digest,
234
+ bytes: buf.length,
235
+ immutable: true,
236
+ });
237
+ rewrites[`/${logical}`] = `/${assetRel}`;
238
+ rewrites[logical] = assetRel;
239
+ }
91
240
  /**
92
241
  * Resolve an immutable object by digest under dist/assets (cross-source reuse).
93
242
  * @param {string} distDir
@@ -123,7 +272,6 @@ export function assertSharedAssetPath(distDir, a, b, ext = '.js') {
123
272
  const rel = `assets/${da}${ext}`;
124
273
  const dest = path.join(distDir, ...rel.split('/'));
125
274
  fs.writeFileSync(dest, typeof a === 'string' ? Buffer.from(a) : a);
126
- // Second write of identical bytes must be reuse, not fork.
127
275
  fs.writeFileSync(dest, typeof b === 'string' ? Buffer.from(b) : b);
128
276
  const again = resolveAssetByDigest(distDir, da, ext);
129
277
  if (!again || again.assetPath !== rel) {
@@ -138,7 +286,6 @@ function collectCandidates(distDir) {
138
286
  if (fs.existsSync(path.join(distDir, name)))
139
287
  out.push(name);
140
288
  }
141
- // Include top-level *.client.js and pages/**/*.client.js referenced by resume.
142
289
  walk(distDir, distDir, (rel) => {
143
290
  if (/\.client\.js$/i.test(rel))
144
291
  out.push(rel);
@@ -172,7 +319,6 @@ function rewriteHtmlReferences(distDir, rewrites) {
172
319
  let text = fs.readFileSync(file, 'utf8');
173
320
  let next = text;
174
321
  for (const [from, to] of pairs) {
175
- // href="/x" src="/x" and unquoted variants in attributes
176
322
  next = next.split(from).join(to);
177
323
  }
178
324
  if (next !== text) {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export declare const DELIVERY_PROFILE_AUTHORING_SCHEMA = "vmz.delivery.authoring.v0";
6
6
  export declare const BUILD_PROFILE_SELECTION_SCHEMA = "vmz.build.profile_selection.v0";
7
- /** Browser-era assembly kinds (04 B5). */
7
+ /** Browser-era assembly kinds (04 B5). `static-cdn` was renamed to `web-static`. */
8
8
  export declare const ASSEMBLIES: readonly string[];
9
9
  export declare const SERVER_RUNTIMES: readonly string[];
10
10
  /** Official built-in aliases when not overridden in config. */
@@ -13,7 +13,7 @@ export declare const BUILTIN_PROFILES: Readonly<{
13
13
  host: string;
14
14
  assembly: string;
15
15
  };
16
- 'web-static': {
16
+ static: {
17
17
  host: string;
18
18
  assembly: string;
19
19
  };
@@ -28,10 +28,36 @@ export declare const BUILTIN_PROFILES: Readonly<{
28
28
  serverRuntime: string;
29
29
  };
30
30
  }>;
31
+ /**
32
+ * Profile artifact directory name under CLI `--out-dir` (default = profile id).
33
+ * @param {string} id
34
+ * @param {unknown} rawName
35
+ * @param {Array<{ code: string, message: string }>} diagnostics
36
+ * @returns {string | null}
37
+ */
38
+ export declare function normalizeProfileArtifactName(id: any, rawName: any, diagnostics: any): string;
39
+ /**
40
+ * Workspace `--out-dir` + profile `name` → artifact root.
41
+ * Always nests: `path.join(outDir, name)` where `name` defaults to profile id
42
+ * (`name: 'cdn'` → `dist/cdn`; omit → `dist/static` for profile `static`).
43
+ * @param {string} outDir
44
+ * @param {{ name?: string, id?: string } | null | undefined} profile
45
+ */
46
+ export declare function resolveProfileArtifactDir(outDir: any, profile: any): any;
31
47
  export declare function pickSiteAuthoring(raw: any): {
32
48
  artifact: string;
33
49
  sources: any;
34
50
  };
51
+ /**
52
+ * `delivery.packaging.wechat` — vendor identity, not WeChat JSON / wx APIs.
53
+ * @param {unknown} raw
54
+ * @param {Array<{ code: string, message: string }>} diagnostics
55
+ */
56
+ export declare function pickDeliveryPackaging(raw: any, diagnostics: any): {
57
+ wechat?: undefined;
58
+ } | {
59
+ wechat: {};
60
+ };
35
61
  export declare function normalizeDeliveryAuthoring(raw: any): {
36
62
  ok: boolean;
37
63
  table: {
@@ -59,6 +85,8 @@ export declare function selectBuildProfile(table: any, cliProfile?: string): {
59
85
  selection: {
60
86
  schema: string;
61
87
  profileId: any;
88
+ name: any;
89
+ nameExplicit: boolean;
62
90
  host: any;
63
91
  assembly: any;
64
92
  serverRuntime: any;
@@ -4,18 +4,67 @@
4
4
  */
5
5
  // @ts-nocheck
6
6
  import crypto from 'node:crypto';
7
+ import path from 'node:path';
7
8
  export const DELIVERY_PROFILE_AUTHORING_SCHEMA = 'vmz.delivery.authoring.v0';
8
9
  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']);
10
+ /** Browser-era assembly kinds (04 B5). `static-cdn` was renamed to `web-static`. */
11
+ export const ASSEMBLIES = Object.freeze(['local-static', 'web-static', 'server-host', 'cdn+server', 'rust-embedded']);
11
12
  export const SERVER_RUNTIMES = Object.freeze(['node', 'worker', 'deno', 'bun', 'rust-host']);
12
13
  /** Official built-in aliases when not overridden in config. */
13
14
  export const BUILTIN_PROFILES = Object.freeze({
14
15
  'web-client': { host: 'browser', assembly: 'local-static' },
15
- 'web-static': { host: 'browser', assembly: 'static-cdn' },
16
+ static: { host: 'browser', assembly: 'web-static' },
16
17
  'web-ssr': { host: 'browser', assembly: 'server-host', serverRuntime: 'node' },
17
18
  'web-hybrid': { host: 'browser', assembly: 'cdn+server', serverRuntime: 'node' },
18
19
  });
20
+ /**
21
+ * Profile artifact directory name under CLI `--out-dir` (default = profile id).
22
+ * @param {string} id
23
+ * @param {unknown} rawName
24
+ * @param {Array<{ code: string, message: string }>} diagnostics
25
+ * @returns {string | null}
26
+ */
27
+ export function normalizeProfileArtifactName(id, rawName, diagnostics) {
28
+ const fallback = String(id || '').trim();
29
+ if (rawName == null || rawName === '')
30
+ return fallback || null;
31
+ if (typeof rawName !== 'string') {
32
+ diagnostics.push({
33
+ code: 'delivery.profile.name',
34
+ message: `profiles.${id}.name must be a string (got ${typeof rawName})`,
35
+ });
36
+ return null;
37
+ }
38
+ const name = rawName.trim();
39
+ if (!name) {
40
+ diagnostics.push({
41
+ code: 'delivery.profile.name',
42
+ message: `profiles.${id}.name must be a non-empty string`,
43
+ });
44
+ return null;
45
+ }
46
+ if (name === '.' || name === '..' || name.includes('/') || name.includes('\\') || name.includes('\0')) {
47
+ diagnostics.push({
48
+ code: 'delivery.profile.name',
49
+ message: `profiles.${id}.name must be a single path segment under --out-dir (got '${name}')`,
50
+ });
51
+ return null;
52
+ }
53
+ return name;
54
+ }
55
+ /**
56
+ * Workspace `--out-dir` + profile `name` → artifact root.
57
+ * Always nests: `path.join(outDir, name)` where `name` defaults to profile id
58
+ * (`name: 'cdn'` → `dist/cdn`; omit → `dist/static` for profile `static`).
59
+ * @param {string} outDir
60
+ * @param {{ name?: string, id?: string } | null | undefined} profile
61
+ */
62
+ export function resolveProfileArtifactDir(outDir, profile) {
63
+ const name = String(profile?.name || profile?.id || '').trim();
64
+ if (!name)
65
+ return outDir;
66
+ return path.join(outDir, name);
67
+ }
19
68
  function isPlainObject(v) {
20
69
  return v != null && typeof v === 'object' && !Array.isArray(v);
21
70
  }
@@ -49,6 +98,67 @@ export function pickSiteAuthoring(raw) {
49
98
  }
50
99
  return site;
51
100
  }
101
+ /**
102
+ * `delivery.packaging.wechat` — vendor identity, not WeChat JSON / wx APIs.
103
+ * @param {unknown} raw
104
+ * @param {Array<{ code: string, message: string }>} diagnostics
105
+ */
106
+ export function pickDeliveryPackaging(raw, diagnostics) {
107
+ if (!isPlainObject(raw) || raw.packaging == null)
108
+ return null;
109
+ if (!isPlainObject(raw.packaging)) {
110
+ diagnostics.push({ code: 'delivery.packaging', message: 'delivery.packaging must be an object' });
111
+ return null;
112
+ }
113
+ for (const key of Object.keys(raw.packaging)) {
114
+ if (key !== 'wechat') {
115
+ diagnostics.push({
116
+ code: 'delivery.packaging.vendor',
117
+ message: `delivery.packaging.${key} is not a known vendor (wechat)`,
118
+ });
119
+ }
120
+ }
121
+ const wechat = raw.packaging.wechat;
122
+ if (wechat == null)
123
+ return {};
124
+ if (!isPlainObject(wechat)) {
125
+ diagnostics.push({
126
+ code: 'delivery.packaging.wechat',
127
+ message: 'delivery.packaging.wechat must be an object',
128
+ });
129
+ return null;
130
+ }
131
+ for (const [k, v] of Object.entries(wechat)) {
132
+ if (typeof v === 'function') {
133
+ diagnostics.push({
134
+ code: 'delivery.packaging.executable',
135
+ message: `delivery.packaging.wechat.${k} must be pure data (no functions)`,
136
+ });
137
+ continue;
138
+ }
139
+ if (k !== 'appId' && k !== 'projectName' && k !== 'title') {
140
+ diagnostics.push({
141
+ code: 'delivery.packaging.wechat.field',
142
+ message: `delivery.packaging.wechat.${k} is not a known field (appId|projectName|title)`,
143
+ });
144
+ }
145
+ else if (v != null && typeof v !== 'string') {
146
+ diagnostics.push({
147
+ code: 'delivery.packaging.wechat.type',
148
+ message: `delivery.packaging.wechat.${k} must be a string`,
149
+ });
150
+ }
151
+ }
152
+ const out = {};
153
+ if (typeof wechat.appId === 'string' && wechat.appId.trim())
154
+ out.appId = wechat.appId.trim();
155
+ if (typeof wechat.projectName === 'string' && wechat.projectName.trim()) {
156
+ out.projectName = wechat.projectName.trim();
157
+ }
158
+ if (typeof wechat.title === 'string' && wechat.title.trim())
159
+ out.title = wechat.title.trim();
160
+ return { wechat: out };
161
+ }
52
162
  function normalizeProfileEntry(entry, id, diagnostics) {
53
163
  if (!isPlainObject(entry)) {
54
164
  diagnostics.push({ code: 'delivery.profile.invalid', message: `profiles.${id} must be an object` });
@@ -61,7 +171,14 @@ function normalizeProfileEntry(entry, id, diagnostics) {
61
171
  message: `profiles.${id}.host: only 'browser' is supported before Browser Production (got ${host})`,
62
172
  });
63
173
  }
64
- const assembly = String(entry.assembly || '').trim();
174
+ let assembly = String(entry.assembly || '').trim();
175
+ if (assembly === 'static-cdn') {
176
+ diagnostics.push({
177
+ code: 'delivery.profile.assembly.renamed',
178
+ message: `profiles.${id}.assembly 'static-cdn' was renamed to 'web-static'`,
179
+ });
180
+ return null;
181
+ }
65
182
  if (!ASSEMBLIES.includes(assembly)) {
66
183
  diagnostics.push({
67
184
  code: 'delivery.profile.assembly',
@@ -69,6 +186,10 @@ function normalizeProfileEntry(entry, id, diagnostics) {
69
186
  });
70
187
  return null;
71
188
  }
189
+ const nameExplicit = entry.name != null && String(entry.name).trim() !== '';
190
+ const name = normalizeProfileArtifactName(id, nameExplicit ? entry.name : id, diagnostics);
191
+ if (!name)
192
+ return null;
72
193
  let serverRuntime = null;
73
194
  if (assembly === 'server-host' || assembly === 'cdn+server') {
74
195
  serverRuntime = String(entry.serverRuntime || 'node');
@@ -110,6 +231,9 @@ function normalizeProfileEntry(entry, id, diagnostics) {
110
231
  }
111
232
  return {
112
233
  id,
234
+ name,
235
+ /** True when author set `profiles.<id>.name`; false → `name` defaulted to profile id. */
236
+ nameExplicit,
113
237
  host: 'browser',
114
238
  assembly,
115
239
  serverRuntime,
@@ -183,6 +307,10 @@ export function normalizeDeliveryAuthoring(raw) {
183
307
  },
184
308
  };
185
309
  }
310
+ else if (isPlainObject(raw.packaging)) {
311
+ defaultId = String(raw.default || 'web-ssr').trim() || 'web-ssr';
312
+ profileInputs = { ...BUILTIN_PROFILES };
313
+ }
186
314
  else {
187
315
  return {
188
316
  ok: false,
@@ -206,6 +334,9 @@ export function normalizeDeliveryAuthoring(raw) {
206
334
  message: `delivery.default '${defaultId}' is not a known profile`,
207
335
  });
208
336
  }
337
+ if (diagnostics.length)
338
+ return { ok: false, diagnostics };
339
+ const packaging = pickDeliveryPackaging(raw, diagnostics);
209
340
  if (diagnostics.length)
210
341
  return { ok: false, diagnostics };
211
342
  const table = {
@@ -213,6 +344,7 @@ export function normalizeDeliveryAuthoring(raw) {
213
344
  default: defaultId,
214
345
  profiles,
215
346
  sugar,
347
+ ...(packaging ? { packaging } : {}),
216
348
  };
217
349
  table.digest = sha256Hex(canonicalJson(table));
218
350
  return { ok: true, table };
@@ -234,6 +366,8 @@ export function selectBuildProfile(table, cliProfile = '') {
234
366
  const selection = {
235
367
  schema: BUILD_PROFILE_SELECTION_SCHEMA,
236
368
  profileId: id,
369
+ name: profile.name,
370
+ nameExplicit: Boolean(profile.nameExplicit),
237
371
  host: profile.host,
238
372
  assembly: profile.assembly,
239
373
  serverRuntime: profile.serverRuntime,
@@ -246,7 +380,7 @@ export function selectBuildProfile(table, cliProfile = '') {
246
380
  }
247
381
  export function semanticIdsForAssembly(assembly) {
248
382
  switch (assembly) {
249
- case 'static-cdn':
383
+ case 'web-static':
250
384
  return ['static-delivery', 'asset-graph'];
251
385
  case 'server-host':
252
386
  return ['server-host', 'asset-graph'];
@@ -16,6 +16,7 @@
16
16
  * @property {string} [host]
17
17
  * @property {number} [port]
18
18
  * @property {number} [pollMs]
19
+ * @property {'browser' | 'mini-program-wechat'} [target]
19
20
  * @property {AbortSignal} [signal]
20
21
  * @property {typeof createWorkspace} [createWorkspaceFn]
21
22
  * @property {(opts: { project: string, outDir: string, host: string, port: number }) => import('node:child_process').ChildProcess} [spawnHostFn]