@vmz/vmz 0.1.13 → 0.1.14
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/build-assemble.js +18 -7
- package/dist/cdn-policy.js +5 -4
- package/dist/cli.js +106 -38
- package/dist/content-addressed-assets.d.ts +23 -2
- package/dist/content-addressed-assets.js +178 -32
- package/dist/delivery-profile.d.ts +30 -2
- package/dist/delivery-profile.js +139 -5
- package/dist/dev-session.d.ts +1 -0
- package/dist/dev-session.js +269 -65
- package/dist/dev-watch-roots.d.ts +80 -0
- package/dist/dev-watch-roots.js +245 -0
- package/dist/document-build.d.ts +2 -1
- package/dist/document-build.js +104 -82
- package/dist/document-cmd.js +2 -9
- package/dist/document-enrich.d.ts +2 -1
- package/dist/document-enrich.js +15 -4
- package/dist/document-integrate.js +29 -21
- package/dist/document-layout-render.d.ts +20 -0
- package/dist/document-layout-render.js +87 -0
- package/dist/document-routing-config.d.ts +13 -0
- package/dist/document-routing-config.js +40 -0
- package/dist/embedded-packaging.js +2 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +63 -4
- package/dist/locale-check.js +28 -61
- package/dist/locale-cmd.js +10 -42
- package/dist/locale-route-emit.d.ts +4 -1
- package/dist/locale-route-emit.js +7 -32
- package/dist/locale-router.js +26 -8
- package/dist/native-addon.d.ts +9 -0
- package/dist/native-addon.js +84 -0
- package/dist/pack.js +3 -2
- package/dist/pretty-json.d.ts +19 -0
- package/dist/pretty-json.js +43 -0
- package/dist/production-observability.js +3 -2
- package/dist/production-test-pack.js +4 -3
- package/dist/public-static-assets.d.ts +24 -0
- package/dist/public-static-assets.js +140 -0
- package/dist/release-pack.js +5 -18
- package/dist/route-path.d.ts +35 -0
- package/dist/route-path.js +77 -0
- package/dist/server-artifact.js +4 -3
- package/dist/site-delivery.js +4 -3
- package/dist/site-favicon.d.ts +31 -0
- package/dist/site-favicon.js +140 -0
- package/dist/static-emit.d.ts +21 -0
- package/dist/static-emit.js +134 -97
- package/dist/test-cmd.js +2 -1
- package/dist/wechat-packaging.d.ts +22 -0
- package/dist/wechat-packaging.js +59 -0
- package/package.json +12 -12
package/dist/build-assemble.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* B5 Assemble dispatch + B6 build-proof (per-build semantic id slots).
|
|
3
3
|
*/
|
|
4
4
|
// @ts-nocheck
|
|
5
|
-
import { mkdirSync
|
|
5
|
+
import { mkdirSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { semanticIdsForAssembly, sha256Hex, canonicalJson } from './delivery-profile.js';
|
|
8
8
|
import { emitServerArtifact } from './server-artifact.js';
|
|
9
9
|
import { emitEmbeddedPackaging } from './embedded-packaging.js';
|
|
10
10
|
import { emitSiteDelivery } from './site-delivery.js';
|
|
11
|
+
import { emitSiteFavicon } from './site-favicon.js';
|
|
12
|
+
import { emitPublicStaticAssets } from './public-static-assets.js';
|
|
11
13
|
import { emitWebStatic } from './static-emit.js';
|
|
14
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
12
15
|
export const BUILD_PROOF_SCHEMA = 'vmz.build.proof.v0';
|
|
13
16
|
export const ASSEMBLE_MANIFEST_SCHEMA = 'vmz.assemble.manifest.v0';
|
|
14
17
|
/**
|
|
@@ -25,10 +28,13 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
25
28
|
serverRuntime: selection.serverRuntime || null,
|
|
26
29
|
steps: [],
|
|
27
30
|
};
|
|
28
|
-
if (assembly === 'static
|
|
29
|
-
const staticResult = await emitWebStatic(outDir, {
|
|
31
|
+
if (assembly === 'web-static' || assembly === 'cdn+server') {
|
|
32
|
+
const staticResult = await emitWebStatic(outDir, {
|
|
33
|
+
origin: ctx.origin,
|
|
34
|
+
projectRoot: ctx.projectRoot,
|
|
35
|
+
});
|
|
30
36
|
result.steps.push({
|
|
31
|
-
kind: 'static
|
|
37
|
+
kind: 'web-static',
|
|
32
38
|
digest: staticResult.digest,
|
|
33
39
|
htmlFiles: staticResult.htmlFiles?.length ?? 0,
|
|
34
40
|
skipped: staticResult.skipped?.length ?? 0,
|
|
@@ -39,6 +45,11 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
39
45
|
skipped: staticResult.skipped,
|
|
40
46
|
};
|
|
41
47
|
}
|
|
48
|
+
else if (assembly === 'server-host' || assembly === 'local-static') {
|
|
49
|
+
// SSR / local packs: favicon + opaque public/ for serve-host.
|
|
50
|
+
emitSiteFavicon(outDir, { projectRoot: ctx.projectRoot });
|
|
51
|
+
emitPublicStaticAssets(outDir, { projectRoot: ctx.projectRoot });
|
|
52
|
+
}
|
|
42
53
|
if (assembly === 'local-static') {
|
|
43
54
|
result.steps.push({
|
|
44
55
|
kind: 'local-static',
|
|
@@ -106,7 +117,7 @@ export async function assembleDelivery(outDir, ctx) {
|
|
|
106
117
|
const vmzDir = path.join(outDir, '_vmz');
|
|
107
118
|
mkdirSync(vmzDir, { recursive: true });
|
|
108
119
|
const file = path.join(vmzDir, 'assemble-manifest.json');
|
|
109
|
-
|
|
120
|
+
writePrettyJsonFile(file, result);
|
|
110
121
|
return { manifest: result, path: file };
|
|
111
122
|
}
|
|
112
123
|
/**
|
|
@@ -123,7 +134,7 @@ export function emitBuildProof(outDir, ctx) {
|
|
|
123
134
|
};
|
|
124
135
|
for (const id of semanticIds) {
|
|
125
136
|
if (id === 'static-delivery') {
|
|
126
|
-
const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'static
|
|
137
|
+
const step = (ctx.assemble?.steps || []).find((s) => s.kind === 'web-static');
|
|
127
138
|
slots[id] = step
|
|
128
139
|
? { status: 'emitted', detail: `digest=${String(step.digest).slice(0, 12)}` }
|
|
129
140
|
: { status: 'pending', detail: 'assembly requires static emit' };
|
|
@@ -186,6 +197,6 @@ export function emitBuildProof(outDir, ctx) {
|
|
|
186
197
|
const vmzDir = path.join(outDir, '_vmz');
|
|
187
198
|
mkdirSync(vmzDir, { recursive: true });
|
|
188
199
|
const file = path.join(vmzDir, 'build-proof.json');
|
|
189
|
-
|
|
200
|
+
writePrettyJsonFile(file, body);
|
|
190
201
|
return { proof: body, path: file };
|
|
191
202
|
}
|
package/dist/cdn-policy.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { assertLocaleCacheKey, localeAwareCacheKey } from './locale-router.js';
|
|
11
|
+
import { writePrettyJsonFile } from './pretty-json.js';
|
|
11
12
|
export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
|
|
12
13
|
export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
|
|
13
14
|
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
@@ -85,7 +86,7 @@ export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
|
85
86
|
const body = {
|
|
86
87
|
schema: CDN_POLICY_MANIFEST_SCHEMA,
|
|
87
88
|
applicationId: staticManifest.applicationId || null,
|
|
88
|
-
deliveryProfile: '
|
|
89
|
+
deliveryProfile: 'static',
|
|
89
90
|
origin,
|
|
90
91
|
spaFallback: false,
|
|
91
92
|
staticManifestDigest: staticManifest.manifestDigest || null,
|
|
@@ -145,14 +146,14 @@ export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
|
145
146
|
const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
|
|
146
147
|
const vmzDir = path.join(distDir, '_vmz');
|
|
147
148
|
fs.mkdirSync(vmzDir, { recursive: true });
|
|
148
|
-
|
|
149
|
+
writePrettyJsonFile(path.join(vmzDir, 'cdn-policy-manifest.json'), policy);
|
|
149
150
|
const local = projectCdnAdapter(policy, 'local-static');
|
|
150
151
|
const netlify = projectCdnAdapter(policy, 'netlify');
|
|
151
152
|
const adaptersDir = path.join(vmzDir, 'adapters');
|
|
152
153
|
fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
|
|
153
154
|
fs.mkdirSync(path.join(adaptersDir, 'netlify'), { recursive: true });
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
writePrettyJsonFile(path.join(adaptersDir, 'local-static', 'projection.json'), local);
|
|
156
|
+
writePrettyJsonFile(path.join(adaptersDir, 'netlify', 'projection.json'), netlify);
|
|
156
157
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
|
|
157
158
|
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
|
|
158
159
|
return { policy, adapters: { 'local-static': local, netlify } };
|
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Node CLI command implementations .
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntimeDist } from './index.js';
|
|
8
|
+
import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, materializeServeHostRuntime, resolveCoreRuntimeDist, resolveNativePath, } from './index.js';
|
|
9
9
|
import { createDevSession } from './dev-session.js';
|
|
10
10
|
import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
|
|
11
11
|
import { log } from './log.js';
|
|
@@ -23,7 +23,7 @@ import { cmdRefactor } from './refactor-cmd.js';
|
|
|
23
23
|
import { cmdExplain } from './explain-cmd.js';
|
|
24
24
|
import { resolveNativeVmzCli } from './resolve-native-cli.js';
|
|
25
25
|
import { loadVmzConfig } from './plugin-host.js';
|
|
26
|
-
import { normalizeDeliveryAuthoring, selectBuildProfile } from './delivery-profile.js';
|
|
26
|
+
import { normalizeDeliveryAuthoring, resolveProfileArtifactDir, selectBuildProfile } from './delivery-profile.js';
|
|
27
27
|
import { packFromDeploymentIr } from './pack.js';
|
|
28
28
|
import { assembleDelivery, emitBuildProof } from './build-assemble.js';
|
|
29
29
|
/**
|
|
@@ -104,10 +104,10 @@ export function printProjectHelp() {
|
|
|
104
104
|
Usage:
|
|
105
105
|
vmz new|init <dir> Scaffold a minimal app (native CLI)
|
|
106
106
|
vmz check [path] Check project via Workspace
|
|
107
|
-
vmz build [path] [options] Build project via Workspace
|
|
107
|
+
vmz build [path] [options] Build project via Workspace; --target mini-program-wechat packs dist/wechat
|
|
108
108
|
vmz serve [path] [options] Serve dist (optional --build)
|
|
109
|
-
vmz dev [path] [options]
|
|
110
|
-
vmz format [path] [--check] Format .vmz via N-API (oxc
|
|
109
|
+
vmz dev [path] [options] Rebuild session; --target mini-program-wechat packs dist/wechat
|
|
110
|
+
vmz format [path] [--check] Format .vmz via N-API (oxc formatter + EditorConfig)
|
|
111
111
|
vmz lint [path] [--deny-warnings] Lint (= check) via N-API
|
|
112
112
|
vmz test [path] [options] Native test discover / report
|
|
113
113
|
vmz document|docs <cmd> Project /documents domain
|
|
@@ -121,10 +121,11 @@ Usage:
|
|
|
121
121
|
vmz help Show this help
|
|
122
122
|
|
|
123
123
|
Options:
|
|
124
|
-
--out-dir, -o <dir>
|
|
124
|
+
--out-dir, -o <dir> Workspace output root (default: dist). Profile artifacts land in <out-dir>/<name> (name defaults to profile id; CDN: name:'cdn' → dist/cdn)
|
|
125
125
|
--release Release build (omit serve-host; pack minify slot; proof)
|
|
126
|
-
--profile <name> Delivery profile (default from config; builtins: web-ssr|
|
|
127
|
-
--
|
|
126
|
+
--profile <name> Delivery profile (default from config; builtins: web-ssr|static|web-client|web-hybrid)
|
|
127
|
+
--target <id> browser (default) | mini-program-wechat (pack dist/wechat for WeChat DevTools; build+dev)
|
|
128
|
+
--origin <url> Site origin for web-static canonical/sitemap
|
|
128
129
|
--host <host> Listen host (default: 127.0.0.1)
|
|
129
130
|
--port <port> Listen port (dev: omit = auto from 5173; set = lock)
|
|
130
131
|
--poll-ms <ms> Dev watch poll interval (default: 300)
|
|
@@ -332,28 +333,35 @@ async function runWithPlugins(ws, project, outDir, fn) {
|
|
|
332
333
|
*/
|
|
333
334
|
async function cmdBuild(args) {
|
|
334
335
|
const pathArg = args._[0] ?? '.';
|
|
335
|
-
const
|
|
336
|
+
const targetRaw = typeof args.target === 'string' ? args.target : 'browser';
|
|
337
|
+
if (targetRaw !== 'browser' && targetRaw !== 'mini-program-wechat') {
|
|
338
|
+
log.error(`unknown --target ${targetRaw} (browser | mini-program-wechat)`);
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
const wechatPack = targetRaw === 'mini-program-wechat';
|
|
342
|
+
const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
|
|
336
343
|
path: pathArg,
|
|
337
344
|
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
338
345
|
});
|
|
339
|
-
|
|
346
|
+
const cfg = await loadVmzConfig(project);
|
|
347
|
+
const cliProfile = typeof args.profile === 'string' ? args.profile : '';
|
|
348
|
+
const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
|
|
349
|
+
if (!norm.ok) {
|
|
350
|
+
log.diagnostics(norm.diagnostics ?? []);
|
|
351
|
+
log.error('delivery authoring invalid');
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
const selected = selectBuildProfile(norm.table, cliProfile);
|
|
355
|
+
if (!selected.ok) {
|
|
356
|
+
log.diagnostics(selected.diagnostics ?? []);
|
|
357
|
+
log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
|
|
361
|
+
log.info(`build ${project} → ${outDir}${wechatPack ? ' (target=mini-program-wechat)' : ''} (out-dir=${outDirRoot}, name=${selected.profile.name})`);
|
|
340
362
|
const ws = createWorkspace({ root: project, outDir });
|
|
341
363
|
try {
|
|
342
|
-
|
|
343
|
-
const cliProfile = typeof args.profile === 'string' ? args.profile : '';
|
|
344
|
-
const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
|
|
345
|
-
if (!norm.ok) {
|
|
346
|
-
log.diagnostics(norm.diagnostics ?? []);
|
|
347
|
-
log.error('delivery authoring invalid');
|
|
348
|
-
return 1;
|
|
349
|
-
}
|
|
350
|
-
const selected = selectBuildProfile(norm.table, cliProfile);
|
|
351
|
-
if (!selected.ok) {
|
|
352
|
-
log.diagnostics(selected.diagnostics ?? []);
|
|
353
|
-
log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
|
|
354
|
-
return 1;
|
|
355
|
-
}
|
|
356
|
-
log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly})`);
|
|
364
|
+
log.info(`delivery profile ${selected.selection.profileId} (assembly=${selected.selection.assembly}, name=${selected.profile.name})`);
|
|
357
365
|
const code = await runWithPlugins(ws, project, outDir, () => {
|
|
358
366
|
const report = ws.build(Boolean(args.release));
|
|
359
367
|
const errors = log.diagnostics(report.diagnostics ?? []);
|
|
@@ -394,6 +402,29 @@ async function cmdBuild(args) {
|
|
|
394
402
|
if (!docs.ok)
|
|
395
403
|
return 1;
|
|
396
404
|
}
|
|
405
|
+
if (wechatPack) {
|
|
406
|
+
if (typeof ws.lowerMiniprogramWechatPackaging !== 'function') {
|
|
407
|
+
log.error('wechat pack: workspace missing lowerMiniprogramWechatPackaging');
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
let report;
|
|
411
|
+
try {
|
|
412
|
+
const raw = ws.lowerMiniprogramWechatPackaging();
|
|
413
|
+
report = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
log.error(`wechat pack failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
417
|
+
return 1;
|
|
418
|
+
}
|
|
419
|
+
log.diagnostics(report.diagnostics ?? []);
|
|
420
|
+
if (report.status !== 'ready') {
|
|
421
|
+
log.error(`wechat pack ${report.status || 'failed'}`);
|
|
422
|
+
return 1;
|
|
423
|
+
}
|
|
424
|
+
const packRoot = report.packRoot || 'dist/wechat';
|
|
425
|
+
log.info(`wechat pack ok → ${path.join(project, packRoot)} (open in WeChat DevTools)`);
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
397
428
|
let pack = null;
|
|
398
429
|
try {
|
|
399
430
|
pack = packFromDeploymentIr(outDir, {
|
|
@@ -419,8 +450,8 @@ async function cmdBuild(args) {
|
|
|
419
450
|
const origin = typeof args.origin === 'string' ? args.origin : undefined;
|
|
420
451
|
let assemble = null;
|
|
421
452
|
try {
|
|
422
|
-
if (selected.selection.assembly === 'static
|
|
423
|
-
log.info(`
|
|
453
|
+
if (selected.selection.assembly === 'web-static') {
|
|
454
|
+
log.info(`static emit ${outDir}`);
|
|
424
455
|
}
|
|
425
456
|
assemble = await assembleDelivery(outDir, {
|
|
426
457
|
selection: selected.selection,
|
|
@@ -431,10 +462,11 @@ async function cmdBuild(args) {
|
|
|
431
462
|
siteId: cfg.application?.id || undefined,
|
|
432
463
|
origin,
|
|
433
464
|
pack: pack.manifest,
|
|
465
|
+
projectRoot: project,
|
|
434
466
|
});
|
|
435
467
|
for (const step of assemble.manifest.steps || []) {
|
|
436
|
-
if (step.kind === 'static
|
|
437
|
-
log.info(`
|
|
468
|
+
if (step.kind === 'web-static') {
|
|
469
|
+
log.info(`static ok (${step.htmlFiles} html, ${step.skipped} skipped, digest=${String(step.digest).slice(0, 12)}…)`);
|
|
438
470
|
}
|
|
439
471
|
else if (step.kind === 'site-delivery' && step.digest) {
|
|
440
472
|
log.info(`site-delivery ok (digest=${String(step.digest).slice(0, 12)}…)`);
|
|
@@ -465,20 +497,33 @@ async function cmdServe(args) {
|
|
|
465
497
|
if (code !== 0)
|
|
466
498
|
return code;
|
|
467
499
|
}
|
|
468
|
-
const { project, outDir } = resolveWorkspaceDirs({
|
|
500
|
+
const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
|
|
469
501
|
path: pathArg,
|
|
470
502
|
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
471
503
|
});
|
|
504
|
+
const cfg = await loadVmzConfig(project);
|
|
505
|
+
const cliProfile = typeof args.profile === 'string' ? args.profile : '';
|
|
506
|
+
const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
|
|
507
|
+
if (!norm.ok) {
|
|
508
|
+
log.diagnostics(norm.diagnostics ?? []);
|
|
509
|
+
log.error('delivery authoring invalid');
|
|
510
|
+
return 1;
|
|
511
|
+
}
|
|
512
|
+
const selected = selectBuildProfile(norm.table, cliProfile);
|
|
513
|
+
if (!selected.ok) {
|
|
514
|
+
log.diagnostics(selected.diagnostics ?? []);
|
|
515
|
+
log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
|
|
472
519
|
const hostJs = path.join(outDir, 'vmz-serve-host.mjs');
|
|
473
520
|
if (!existsSync(hostJs)) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (src && existsSync(src)) {
|
|
477
|
-
copyFileSync(src, hostJs);
|
|
521
|
+
try {
|
|
522
|
+
materializeServeHostRuntime(outDir);
|
|
478
523
|
log.info(`materialized ${hostJs} from @vmz/core (release builds omit it)`);
|
|
479
524
|
}
|
|
480
|
-
|
|
481
|
-
log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed`);
|
|
525
|
+
catch (err) {
|
|
526
|
+
log.error(`missing ${hostJs} — run \`vmz build\` (without --release) or ensure @vmz/core is installed (${err instanceof Error ? err.message : err})`);
|
|
482
527
|
return 1;
|
|
483
528
|
}
|
|
484
529
|
}
|
|
@@ -493,6 +538,8 @@ async function cmdServe(args) {
|
|
|
493
538
|
VMZ_DIST: outDir,
|
|
494
539
|
VMZ_PORT: String(port),
|
|
495
540
|
VMZ_HOST: host,
|
|
541
|
+
VMZ_PROJECT_ROOT: project,
|
|
542
|
+
VMZ_NATIVE_NODE: resolveNativePath(),
|
|
496
543
|
},
|
|
497
544
|
stdio: 'inherit',
|
|
498
545
|
});
|
|
@@ -517,10 +564,25 @@ async function cmdServe(args) {
|
|
|
517
564
|
*/
|
|
518
565
|
async function cmdDev(args) {
|
|
519
566
|
const pathArg = args._[0] ?? '.';
|
|
520
|
-
const { project, outDir } = resolveWorkspaceDirs({
|
|
567
|
+
const { project, outDir: outDirRoot } = resolveWorkspaceDirs({
|
|
521
568
|
path: pathArg,
|
|
522
569
|
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
523
570
|
});
|
|
571
|
+
const cfg = await loadVmzConfig(project);
|
|
572
|
+
const cliProfile = typeof args.profile === 'string' ? args.profile : '';
|
|
573
|
+
const norm = normalizeDeliveryAuthoring(cfg.delivery ?? null);
|
|
574
|
+
if (!norm.ok) {
|
|
575
|
+
log.diagnostics(norm.diagnostics ?? []);
|
|
576
|
+
log.error('delivery authoring invalid');
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
const selected = selectBuildProfile(norm.table, cliProfile);
|
|
580
|
+
if (!selected.ok) {
|
|
581
|
+
log.diagnostics(selected.diagnostics ?? []);
|
|
582
|
+
log.error(`unknown build --profile ${cliProfile || norm.table.default}`);
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
const outDir = resolveProfileArtifactDir(outDirRoot, selected.profile);
|
|
524
586
|
const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
|
|
525
587
|
const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
|
|
526
588
|
let port;
|
|
@@ -539,6 +601,11 @@ async function cmdDev(args) {
|
|
|
539
601
|
}
|
|
540
602
|
}
|
|
541
603
|
const pollMs = Number(args['poll-ms'] ?? 300);
|
|
604
|
+
const targetRaw = typeof args.target === 'string' ? args.target : 'browser';
|
|
605
|
+
if (targetRaw !== 'browser' && targetRaw !== 'mini-program-wechat') {
|
|
606
|
+
log.error(`unknown --target ${targetRaw} (browser | mini-program-wechat)`);
|
|
607
|
+
return 1;
|
|
608
|
+
}
|
|
542
609
|
const ac = new AbortController();
|
|
543
610
|
const onSig = () => {
|
|
544
611
|
log.info('shutting down…');
|
|
@@ -552,6 +619,7 @@ async function cmdDev(args) {
|
|
|
552
619
|
host,
|
|
553
620
|
port,
|
|
554
621
|
pollMs,
|
|
622
|
+
target: targetRaw,
|
|
555
623
|
signal: ac.signal,
|
|
556
624
|
});
|
|
557
625
|
try {
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A3: content-addressed assets/<hash> layout for immutable CDN objects.
|
|
3
|
-
* Logical paths stay available for serve/dev;
|
|
4
|
-
*
|
|
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
|
export declare const CONTENT_ADDRESSED_ASSETS_SCHEMA = "vmz.content_addressed_assets.v0";
|
|
9
|
+
/**
|
|
10
|
+
* Rewrite relative `@import "./foo.css"` to hashed paths under assets/.
|
|
11
|
+
* @param {string} cssText
|
|
12
|
+
* @param {Record<string, string>} rewrites logical (no leading slash) or `/logical` → `assets/hash.ext`
|
|
13
|
+
*/
|
|
14
|
+
export declare function rewriteCssImports(cssText: any, rewrites: any): any;
|
|
15
|
+
/**
|
|
16
|
+
* Rewrite relative ESM so a file served from `/assets/<hash>.js` resolves against
|
|
17
|
+
* dist root (static `from "./x"` / `export * from "./x"` + dynamic `import("./"+…)`).
|
|
18
|
+
*
|
|
19
|
+
* Always use `../…` (Fix A). Do **not** prefer hashed siblings under `assets/`:
|
|
20
|
+
* barrels like `vmz-dom.js` (`export * from './dom-core.js'`) would then resolve
|
|
21
|
+
* as `/assets/dom-core.js` and 404. `rewrites` is accepted for API parity with CSS
|
|
22
|
+
* but intentionally ignored for JS path choice.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} jsText
|
|
25
|
+
* @param {Record<string, string>} [_rewrites]
|
|
26
|
+
*/
|
|
27
|
+
export declare function rewriteJsEntryRelativeImports(jsText: any, _rewrites?: {}): string;
|
|
7
28
|
/**
|
|
8
29
|
* Emit `assets/<sha256>.<ext>` copies and rewrite HTML href/src to hashed URLs.
|
|
9
30
|
* @param {string} distDir
|
|
@@ -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;
|
|
4
|
-
*
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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) {
|