@vmz/vmz 0.0.3 → 0.0.4
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/cdn-policy.d.ts +178 -0
- package/dist/cdn-policy.js +344 -0
- package/dist/cli.js +65 -6
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/dev-session.js +48 -13
- package/dist/document-designs.js +30 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +27 -14
- 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 +125 -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 +158 -0
- package/dist/production-test-pack.js +452 -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 +337 -0
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +136 -0
- package/dist/static-emit.js +463 -0
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@vmz/vmz` (bin: `vmz`)
|
|
2
|
+
|
|
3
|
+
CLI / N-API toolchain host. Runtime mental entry is **`@vmz/core`**; optional official UI is **`@vmz/ui`**; plugins are
|
|
4
|
+
**`@vmz/plugin-*`**. Shelved umbrella name: `@vmz/vmz`.
|
|
2
5
|
|
|
3
6
|
When a project reaches the point where it has a browser surface, SSR, server work, documents, tests, and several
|
|
4
7
|
deployment concerns, the usual workflow becomes a chain of tools that each see a different version of the application. A
|
|
@@ -22,9 +25,8 @@ With VMZ, a normal workflow is expected to answer all of these from the same sou
|
|
|
22
25
|
- **Test:** what did a user interaction actually cause?
|
|
23
26
|
- **Explain:** why did the compiler make that decision?
|
|
24
27
|
|
|
25
|
-
Use it when you are adopting VMZ as the application model.
|
|
26
|
-
|
|
27
|
-
toolchains.
|
|
28
|
+
Use it when you are adopting VMZ as the application model. The CLI targets VMZ-native source and semantics; Vue, React,
|
|
29
|
+
and legacy VDOM ecosystems keep their own toolchains.
|
|
28
30
|
|
|
29
31
|
## One tool, several views of the same program
|
|
30
32
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3-cdn: vendor-neutral CDN policy (cache / redirect / error) + local static host.
|
|
3
|
+
* Provider adapters only project the same contract — they must not change RouteId/canonical/CSP.
|
|
4
|
+
*/
|
|
5
|
+
export declare const CDN_POLICY_MANIFEST_SCHEMA = "vmz.cdn.policy_manifest.v0";
|
|
6
|
+
export declare const CDN_ADAPTER_PROJECTION_SCHEMA = "vmz.cdn.adapter_projection.v0";
|
|
7
|
+
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
8
|
+
export declare const CACHE_HTML = "public, max-age=0, must-revalidate";
|
|
9
|
+
export declare const CACHE_ASSET_IMMUTABLE = "public, max-age=31536000, immutable";
|
|
10
|
+
export declare const CACHE_META = "public, max-age=3600";
|
|
11
|
+
/**
|
|
12
|
+
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
|
|
13
|
+
* @param {Record<string, any>} staticManifest
|
|
14
|
+
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildCdnPolicyManifest(staticManifest: any, opts?: {}): {
|
|
17
|
+
schema: string;
|
|
18
|
+
applicationId: any;
|
|
19
|
+
deliveryProfile: string;
|
|
20
|
+
origin: string;
|
|
21
|
+
spaFallback: boolean;
|
|
22
|
+
staticManifestDigest: any;
|
|
23
|
+
redirects: any[];
|
|
24
|
+
headers: ({
|
|
25
|
+
match: string;
|
|
26
|
+
headers: {
|
|
27
|
+
'cache-control': string;
|
|
28
|
+
'x-robots-tag'?: undefined;
|
|
29
|
+
};
|
|
30
|
+
} | {
|
|
31
|
+
match: string;
|
|
32
|
+
headers: {
|
|
33
|
+
'cache-control': string;
|
|
34
|
+
'x-robots-tag': string;
|
|
35
|
+
};
|
|
36
|
+
})[];
|
|
37
|
+
errorDocuments: any;
|
|
38
|
+
routes: any;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Write CDN policy + adapter projections under dist/_vmz.
|
|
42
|
+
* @param {string} distDir
|
|
43
|
+
* @param {Record<string, any>} staticManifest
|
|
44
|
+
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
45
|
+
*/
|
|
46
|
+
export declare function emitCdnPolicy(distDir: any, staticManifest: any, opts?: {}): {
|
|
47
|
+
policy: {
|
|
48
|
+
schema: string;
|
|
49
|
+
applicationId: any;
|
|
50
|
+
deliveryProfile: string;
|
|
51
|
+
origin: string;
|
|
52
|
+
spaFallback: boolean;
|
|
53
|
+
staticManifestDigest: any;
|
|
54
|
+
redirects: any[];
|
|
55
|
+
headers: ({
|
|
56
|
+
match: string;
|
|
57
|
+
headers: {
|
|
58
|
+
'cache-control': string;
|
|
59
|
+
'x-robots-tag'?: undefined;
|
|
60
|
+
};
|
|
61
|
+
} | {
|
|
62
|
+
match: string;
|
|
63
|
+
headers: {
|
|
64
|
+
'cache-control': string;
|
|
65
|
+
'x-robots-tag': string;
|
|
66
|
+
};
|
|
67
|
+
})[];
|
|
68
|
+
errorDocuments: any;
|
|
69
|
+
routes: any;
|
|
70
|
+
};
|
|
71
|
+
adapters: {
|
|
72
|
+
'local-static': {
|
|
73
|
+
schema: string;
|
|
74
|
+
adapterId: string;
|
|
75
|
+
policyDigest: any;
|
|
76
|
+
host: string;
|
|
77
|
+
spaFallback: boolean;
|
|
78
|
+
redirects: any;
|
|
79
|
+
headers: any;
|
|
80
|
+
errorDocuments: any;
|
|
81
|
+
files: {
|
|
82
|
+
_headers?: undefined;
|
|
83
|
+
_redirects?: undefined;
|
|
84
|
+
};
|
|
85
|
+
} | {
|
|
86
|
+
schema: string;
|
|
87
|
+
adapterId: string;
|
|
88
|
+
policyDigest: any;
|
|
89
|
+
host: string;
|
|
90
|
+
spaFallback: boolean;
|
|
91
|
+
redirects: any;
|
|
92
|
+
headers: any;
|
|
93
|
+
errorDocuments: any;
|
|
94
|
+
files: {
|
|
95
|
+
_headers: string;
|
|
96
|
+
_redirects: string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
netlify: {
|
|
100
|
+
schema: string;
|
|
101
|
+
adapterId: string;
|
|
102
|
+
policyDigest: any;
|
|
103
|
+
host: string;
|
|
104
|
+
spaFallback: boolean;
|
|
105
|
+
redirects: any;
|
|
106
|
+
headers: any;
|
|
107
|
+
errorDocuments: any;
|
|
108
|
+
files: {
|
|
109
|
+
_headers?: undefined;
|
|
110
|
+
_redirects?: undefined;
|
|
111
|
+
};
|
|
112
|
+
} | {
|
|
113
|
+
schema: string;
|
|
114
|
+
adapterId: string;
|
|
115
|
+
policyDigest: any;
|
|
116
|
+
host: string;
|
|
117
|
+
spaFallback: boolean;
|
|
118
|
+
redirects: any;
|
|
119
|
+
headers: any;
|
|
120
|
+
errorDocuments: any;
|
|
121
|
+
files: {
|
|
122
|
+
_headers: string;
|
|
123
|
+
_redirects: string;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
|
|
130
|
+
* @param {Record<string, any>} policy
|
|
131
|
+
* @param {'local-static' | 'netlify'} adapterId
|
|
132
|
+
*/
|
|
133
|
+
export declare function projectCdnAdapter(policy: any, adapterId: any): {
|
|
134
|
+
schema: string;
|
|
135
|
+
adapterId: string;
|
|
136
|
+
policyDigest: any;
|
|
137
|
+
host: string;
|
|
138
|
+
spaFallback: boolean;
|
|
139
|
+
redirects: any;
|
|
140
|
+
headers: any;
|
|
141
|
+
errorDocuments: any;
|
|
142
|
+
files: {
|
|
143
|
+
_headers?: undefined;
|
|
144
|
+
_redirects?: undefined;
|
|
145
|
+
};
|
|
146
|
+
} | {
|
|
147
|
+
schema: string;
|
|
148
|
+
adapterId: string;
|
|
149
|
+
policyDigest: any;
|
|
150
|
+
host: string;
|
|
151
|
+
spaFallback: boolean;
|
|
152
|
+
redirects: any;
|
|
153
|
+
headers: any;
|
|
154
|
+
errorDocuments: any;
|
|
155
|
+
files: {
|
|
156
|
+
_headers: string;
|
|
157
|
+
_redirects: string;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Local static host that applies CDNPolicyManifest (redirects, cache headers, 404 doc).
|
|
162
|
+
* @param {string} distDir
|
|
163
|
+
* @param {Record<string, any>} policy
|
|
164
|
+
* @param {{ host?: string, port?: number }} [opts]
|
|
165
|
+
* @returns {Promise<{ host: string, port: number, baseUrl: string, close: () => Promise<void> }>}
|
|
166
|
+
*/
|
|
167
|
+
export declare function listenLocalStaticHost(distDir: any, policy: any, opts?: {}): Promise<unknown>;
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} distDir
|
|
170
|
+
* @param {Record<string, any>} policy
|
|
171
|
+
*/
|
|
172
|
+
export declare function createLocalStaticHandler(distDir: any, policy: any): (req: any, res: any) => void;
|
|
173
|
+
/**
|
|
174
|
+
* Minimal glob matcher for CDN header rules (html / js|css|mjs / exact / prefix).
|
|
175
|
+
* @param {string} pattern
|
|
176
|
+
* @param {string} pathname
|
|
177
|
+
*/
|
|
178
|
+
export declare function matchGlob(pattern: any, pathname: any): any;
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3-cdn: vendor-neutral CDN policy (cache / redirect / error) + local static host.
|
|
3
|
+
* Provider adapters only project the same contract — they must not change RouteId/canonical/CSP.
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
|
|
11
|
+
export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
|
|
12
|
+
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
13
|
+
export const CACHE_HTML = 'public, max-age=0, must-revalidate';
|
|
14
|
+
export const CACHE_ASSET_IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
15
|
+
export const CACHE_META = 'public, max-age=3600';
|
|
16
|
+
/**
|
|
17
|
+
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects).
|
|
18
|
+
* @param {Record<string, any>} staticManifest
|
|
19
|
+
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
20
|
+
*/
|
|
21
|
+
export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
22
|
+
const origin = String(staticManifest.origin || '');
|
|
23
|
+
const redirects = [{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' }, ...(opts.redirects || [])];
|
|
24
|
+
const headers = [
|
|
25
|
+
{ match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
|
|
26
|
+
{
|
|
27
|
+
match: '**/404.html',
|
|
28
|
+
headers: { 'cache-control': CACHE_HTML, 'x-robots-tag': 'noindex, nofollow' },
|
|
29
|
+
},
|
|
30
|
+
// Content-addressed immutable objects (A3 assets/<hash>).
|
|
31
|
+
{ match: '**/assets/**', headers: { 'cache-control': CACHE_ASSET_IMMUTABLE } },
|
|
32
|
+
{ match: '**/*.{js,css,mjs}', headers: { 'cache-control': CACHE_ASSET_IMMUTABLE } },
|
|
33
|
+
{ match: '**/sitemap.xml', headers: { 'cache-control': CACHE_META } },
|
|
34
|
+
{ match: '**/robots.txt', headers: { 'cache-control': CACHE_META } },
|
|
35
|
+
];
|
|
36
|
+
const errorDocuments = Array.isArray(staticManifest.errorDocuments) ? staticManifest.errorDocuments : [{ status: 404, path: '404.html' }];
|
|
37
|
+
const body = {
|
|
38
|
+
schema: CDN_POLICY_MANIFEST_SCHEMA,
|
|
39
|
+
applicationId: staticManifest.applicationId || null,
|
|
40
|
+
deliveryProfile: 'web-static',
|
|
41
|
+
origin,
|
|
42
|
+
spaFallback: false,
|
|
43
|
+
staticManifestDigest: staticManifest.manifestDigest || null,
|
|
44
|
+
redirects,
|
|
45
|
+
headers,
|
|
46
|
+
errorDocuments,
|
|
47
|
+
routes: (staticManifest.routes || []).map((r) => ({
|
|
48
|
+
routeId: r.routeId,
|
|
49
|
+
path: r.path,
|
|
50
|
+
htmlPath: r.htmlPath,
|
|
51
|
+
canonical: r.seo?.canonical || null,
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
body.policyDigest = sha256Hex(canonicalJson(body));
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write CDN policy + adapter projections under dist/_vmz.
|
|
59
|
+
* @param {string} distDir
|
|
60
|
+
* @param {Record<string, any>} staticManifest
|
|
61
|
+
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
62
|
+
*/
|
|
63
|
+
export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
64
|
+
const policy = buildCdnPolicyManifest(staticManifest, opts);
|
|
65
|
+
const vmzDir = path.join(distDir, '_vmz');
|
|
66
|
+
fs.mkdirSync(vmzDir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
|
|
68
|
+
const local = projectCdnAdapter(policy, 'local-static');
|
|
69
|
+
const netlify = projectCdnAdapter(policy, 'netlify');
|
|
70
|
+
const adaptersDir = path.join(vmzDir, 'adapters');
|
|
71
|
+
fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
|
|
72
|
+
fs.mkdirSync(path.join(adaptersDir, 'netlify'), { recursive: true });
|
|
73
|
+
fs.writeFileSync(path.join(adaptersDir, 'local-static', 'projection.json'), `${JSON.stringify(local, null, 2)}\n`, 'utf8');
|
|
74
|
+
fs.writeFileSync(path.join(adaptersDir, 'netlify', 'projection.json'), `${JSON.stringify(netlify, null, 2)}\n`, 'utf8');
|
|
75
|
+
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
|
|
76
|
+
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
|
|
77
|
+
return { policy, adapters: { 'local-static': local, netlify } };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
|
|
81
|
+
* @param {Record<string, any>} policy
|
|
82
|
+
* @param {'local-static' | 'netlify'} adapterId
|
|
83
|
+
*/
|
|
84
|
+
export function projectCdnAdapter(policy, adapterId) {
|
|
85
|
+
if (policy.spaFallback) {
|
|
86
|
+
throw new Error('projectCdnAdapter: spaFallback=true is forbidden');
|
|
87
|
+
}
|
|
88
|
+
if (adapterId === 'local-static') {
|
|
89
|
+
return {
|
|
90
|
+
schema: CDN_ADAPTER_PROJECTION_SCHEMA,
|
|
91
|
+
adapterId: 'local-static',
|
|
92
|
+
policyDigest: policy.policyDigest,
|
|
93
|
+
host: 'vmz-local-static',
|
|
94
|
+
spaFallback: false,
|
|
95
|
+
redirects: policy.redirects,
|
|
96
|
+
headers: policy.headers,
|
|
97
|
+
errorDocuments: policy.errorDocuments,
|
|
98
|
+
files: {},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (adapterId === 'netlify') {
|
|
102
|
+
const headerLines = [];
|
|
103
|
+
for (const rule of policy.headers || []) {
|
|
104
|
+
const glob = netlifyGlob(rule.match);
|
|
105
|
+
headerLines.push(glob);
|
|
106
|
+
for (const [k, v] of Object.entries(rule.headers || {})) {
|
|
107
|
+
headerLines.push(` ${headerCase(k)}: ${v}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const redirectLines = [];
|
|
111
|
+
for (const r of policy.redirects || []) {
|
|
112
|
+
redirectLines.push(`${r.from} ${r.to} ${Number(r.status) || 301}`);
|
|
113
|
+
}
|
|
114
|
+
// Explicit only — never `/* /index.html 200`
|
|
115
|
+
const redirectsText = redirectLines.join('\n') + (redirectLines.length ? '\n' : '');
|
|
116
|
+
if (/\*\s+\/index\.html/.test(redirectsText)) {
|
|
117
|
+
throw new Error('netlify adapter refused SPA fallback redirect');
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
schema: CDN_ADAPTER_PROJECTION_SCHEMA,
|
|
121
|
+
adapterId: 'netlify',
|
|
122
|
+
policyDigest: policy.policyDigest,
|
|
123
|
+
host: 'netlify',
|
|
124
|
+
spaFallback: false,
|
|
125
|
+
redirects: policy.redirects,
|
|
126
|
+
headers: policy.headers,
|
|
127
|
+
errorDocuments: policy.errorDocuments,
|
|
128
|
+
files: {
|
|
129
|
+
_headers: headerLines.join('\n') + '\n',
|
|
130
|
+
_redirects: redirectsText,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`unknown CDN adapter ${adapterId}`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Local static host that applies CDNPolicyManifest (redirects, cache headers, 404 doc).
|
|
138
|
+
* @param {string} distDir
|
|
139
|
+
* @param {Record<string, any>} policy
|
|
140
|
+
* @param {{ host?: string, port?: number }} [opts]
|
|
141
|
+
* @returns {Promise<{ host: string, port: number, baseUrl: string, close: () => Promise<void> }>}
|
|
142
|
+
*/
|
|
143
|
+
export function listenLocalStaticHost(distDir, policy, opts = {}) {
|
|
144
|
+
const host = opts.host || '127.0.0.1';
|
|
145
|
+
const port = Number(opts.port || 0);
|
|
146
|
+
const handler = createLocalStaticHandler(distDir, policy);
|
|
147
|
+
const server = http.createServer(handler);
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
server.listen(port, host, () => {
|
|
150
|
+
const addr = server.address();
|
|
151
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
152
|
+
resolve({
|
|
153
|
+
host,
|
|
154
|
+
port: actualPort,
|
|
155
|
+
baseUrl: `http://${host}:${actualPort}`,
|
|
156
|
+
close: () => new Promise((res, rej) => {
|
|
157
|
+
server.close((err) => (err ? rej(err) : res()));
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
server.on('error', reject);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} distDir
|
|
166
|
+
* @param {Record<string, any>} policy
|
|
167
|
+
*/
|
|
168
|
+
export function createLocalStaticHandler(distDir, policy) {
|
|
169
|
+
const root = path.resolve(distDir);
|
|
170
|
+
return (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
173
|
+
let pathname = decodeURIComponent(url.pathname || '/');
|
|
174
|
+
const redirect = matchRedirect(policy.redirects || [], pathname);
|
|
175
|
+
if (redirect) {
|
|
176
|
+
const status = Number(redirect.status) || 301;
|
|
177
|
+
const headers = applyHeaderRules(policy.headers || [], pathname, {
|
|
178
|
+
Location: redirect.to,
|
|
179
|
+
});
|
|
180
|
+
res.writeHead(status, headers);
|
|
181
|
+
res.end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let rel = pathname;
|
|
185
|
+
if (rel.endsWith('/'))
|
|
186
|
+
rel += 'index.html';
|
|
187
|
+
if (rel === '/')
|
|
188
|
+
rel = '/index.html';
|
|
189
|
+
const file = safeJoin(root, rel.replace(/^\//, ''));
|
|
190
|
+
if (file && fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
191
|
+
const body = fs.readFileSync(file);
|
|
192
|
+
const type = contentType(file);
|
|
193
|
+
const headers = applyHeaderRules(policy.headers || [], rel, {
|
|
194
|
+
'content-type': type,
|
|
195
|
+
});
|
|
196
|
+
res.writeHead(200, headers);
|
|
197
|
+
res.end(body);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const errDoc = (policy.errorDocuments || []).find((e) => Number(e.status) === 404);
|
|
201
|
+
const errPath = errDoc?.path || '404.html';
|
|
202
|
+
const abs404 = path.join(root, errPath);
|
|
203
|
+
if (fs.existsSync(abs404)) {
|
|
204
|
+
const body = fs.readFileSync(abs404);
|
|
205
|
+
const headers = applyHeaderRules(policy.headers || [], `/${errPath}`, {
|
|
206
|
+
'content-type': 'text/html; charset=utf-8',
|
|
207
|
+
});
|
|
208
|
+
res.writeHead(404, headers);
|
|
209
|
+
res.end(body);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
213
|
+
res.end('not found');
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
217
|
+
res.end(err instanceof Error ? err.message : String(err));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* @param {Array<{ from: string, to: string, status?: number }>} redirects
|
|
223
|
+
* @param {string} pathname
|
|
224
|
+
*/
|
|
225
|
+
function matchRedirect(redirects, pathname) {
|
|
226
|
+
const p = pathname.replace(/\/+$/, '') || '/';
|
|
227
|
+
for (const r of redirects) {
|
|
228
|
+
const from = String(r.from || '').replace(/\/+$/, '') || '/';
|
|
229
|
+
if (from === p || from === pathname)
|
|
230
|
+
return r;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* @param {Array<{ match: string, headers: Record<string, string> }>} rules
|
|
236
|
+
* @param {string} pathname
|
|
237
|
+
* @param {Record<string, string>} base
|
|
238
|
+
*/
|
|
239
|
+
function applyHeaderRules(rules, pathname, base = {}) {
|
|
240
|
+
/** @type {Record<string, string>} */
|
|
241
|
+
const out = { ...base };
|
|
242
|
+
for (const rule of rules) {
|
|
243
|
+
if (matchGlob(rule.match, pathname)) {
|
|
244
|
+
Object.assign(out, rule.headers || {});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Minimal glob matcher for CDN header rules (html / js|css|mjs / exact / prefix).
|
|
251
|
+
* @param {string} pattern
|
|
252
|
+
* @param {string} pathname
|
|
253
|
+
*/
|
|
254
|
+
export function matchGlob(pattern, pathname) {
|
|
255
|
+
const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
256
|
+
const pat = String(pattern || '');
|
|
257
|
+
if (pat === '**/*.html')
|
|
258
|
+
return p.endsWith('.html');
|
|
259
|
+
if (pat === '**/404.html')
|
|
260
|
+
return p === '/404.html' || p.endsWith('/404.html');
|
|
261
|
+
if (pat === '**/assets/**')
|
|
262
|
+
return p === '/assets' || p.startsWith('/assets/');
|
|
263
|
+
if (pat === '**/*.{js,css,mjs}')
|
|
264
|
+
return /\.(js|css|mjs)$/.test(p);
|
|
265
|
+
if (pat === '**/sitemap.xml')
|
|
266
|
+
return p.endsWith('/sitemap.xml') || p === '/sitemap.xml';
|
|
267
|
+
if (pat === '**/robots.txt')
|
|
268
|
+
return p.endsWith('/robots.txt') || p === '/robots.txt';
|
|
269
|
+
if (pat.endsWith('/**')) {
|
|
270
|
+
const prefix = pat.slice(0, -3);
|
|
271
|
+
// Only treat as path prefix when pattern is absolute-ish (starts with / or bare segment).
|
|
272
|
+
if (prefix.startsWith('/')) {
|
|
273
|
+
return p === prefix || p.startsWith(prefix + '/');
|
|
274
|
+
}
|
|
275
|
+
if (!prefix.includes('*')) {
|
|
276
|
+
return p === `/${prefix}` || p.startsWith(`/${prefix}/`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return p === pat || p === `/${pat.replace(/^\//, '')}`;
|
|
280
|
+
}
|
|
281
|
+
function netlifyGlob(match) {
|
|
282
|
+
if (match === '**/*.html')
|
|
283
|
+
return '/*.html';
|
|
284
|
+
if (match === '**/404.html')
|
|
285
|
+
return '/404.html';
|
|
286
|
+
if (match === '**/assets/**')
|
|
287
|
+
return '/assets/*';
|
|
288
|
+
if (match === '**/*.{js,css,mjs}')
|
|
289
|
+
return '/*.{js,css,mjs}';
|
|
290
|
+
if (match === '**/sitemap.xml')
|
|
291
|
+
return '/sitemap.xml';
|
|
292
|
+
if (match === '**/robots.txt')
|
|
293
|
+
return '/robots.txt';
|
|
294
|
+
return match.startsWith('/') ? match : `/${match}`;
|
|
295
|
+
}
|
|
296
|
+
function headerCase(name) {
|
|
297
|
+
return String(name)
|
|
298
|
+
.split('-')
|
|
299
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
300
|
+
.join('-');
|
|
301
|
+
}
|
|
302
|
+
function contentType(file) {
|
|
303
|
+
if (file.endsWith('.html'))
|
|
304
|
+
return 'text/html; charset=utf-8';
|
|
305
|
+
if (file.endsWith('.js') || file.endsWith('.mjs'))
|
|
306
|
+
return 'text/javascript; charset=utf-8';
|
|
307
|
+
if (file.endsWith('.css'))
|
|
308
|
+
return 'text/css; charset=utf-8';
|
|
309
|
+
if (file.endsWith('.xml'))
|
|
310
|
+
return 'application/xml';
|
|
311
|
+
if (file.endsWith('.txt'))
|
|
312
|
+
return 'text/plain; charset=utf-8';
|
|
313
|
+
if (file.endsWith('.json'))
|
|
314
|
+
return 'application/json; charset=utf-8';
|
|
315
|
+
return 'application/octet-stream';
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* @param {string} root
|
|
319
|
+
* @param {string} rel
|
|
320
|
+
*/
|
|
321
|
+
function safeJoin(root, rel) {
|
|
322
|
+
const full = path.resolve(root, rel);
|
|
323
|
+
const normRoot = path.resolve(root);
|
|
324
|
+
if (full !== normRoot && !full.startsWith(normRoot + path.sep))
|
|
325
|
+
return null;
|
|
326
|
+
return full;
|
|
327
|
+
}
|
|
328
|
+
function sha256Hex(data) {
|
|
329
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
330
|
+
}
|
|
331
|
+
function canonicalJson(value) {
|
|
332
|
+
return JSON.stringify(sortKeys(value));
|
|
333
|
+
}
|
|
334
|
+
function sortKeys(value) {
|
|
335
|
+
if (Array.isArray(value))
|
|
336
|
+
return value.map(sortKeys);
|
|
337
|
+
if (value && typeof value === 'object') {
|
|
338
|
+
const out = {};
|
|
339
|
+
for (const k of Object.keys(value).sort())
|
|
340
|
+
out[k] = sortKeys(value[k]);
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
return value;
|
|
344
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -9,15 +9,21 @@ import { HOST_PROTOCOL, createWorkspace, getProtocolVersions, resolveCoreRuntime
|
|
|
9
9
|
import { createDevSession } from './dev-session.js';
|
|
10
10
|
import { gateGlobalProjectCommand, getInvocationContext, isGlobalAllowedCommand } from './invocation.js';
|
|
11
11
|
import { log } from './log.js';
|
|
12
|
+
import { findAvailablePort } from './port.js';
|
|
12
13
|
import { readPackageMeta, resolveWorkspaceDirs } from './resolve.js';
|
|
13
14
|
import { cmdTest } from './test-cmd.js';
|
|
14
15
|
import { cmdDocument } from './document-cmd.js';
|
|
15
16
|
import { buildIntegratedDocuments, projectHasDocuments } from './document-integrate.js';
|
|
16
17
|
import { cmdLocale } from './locale-cmd.js';
|
|
18
|
+
import { emitLocaleRuntimeModules, localeHasErrors } from './locale-check.js';
|
|
17
19
|
import { cmdApplication } from './application-cmd.js';
|
|
20
|
+
import { cmdArtifact } from './release-cmd.js';
|
|
18
21
|
import { cmdRefactor } from './refactor-cmd.js';
|
|
19
22
|
import { cmdExplain } from './explain-cmd.js';
|
|
20
23
|
import { resolveNativeVmzCli } from './resolve-native-cli.js';
|
|
24
|
+
import { emitWebStatic } from './static-emit.js';
|
|
25
|
+
import { emitSiteDelivery } from './site-delivery.js';
|
|
26
|
+
import { loadVmzConfig } from './plugin-host.js';
|
|
21
27
|
/**
|
|
22
28
|
* @param {string[]} argv
|
|
23
29
|
*/
|
|
@@ -66,8 +72,11 @@ export function parseArgs(argv) {
|
|
|
66
72
|
export function printGlobalHelp() {
|
|
67
73
|
console.log(`vmz — global mode (scaffold only)
|
|
68
74
|
|
|
75
|
+
Install faces: @vmz/core (runtime) · @vmz/vmz (this CLI) · optional @vmz/ui / @vmz/plugin-*
|
|
76
|
+
|
|
77
|
+
Three install modes:
|
|
69
78
|
developer monorepo source (packages/runtimes/vmz) — full CLI
|
|
70
|
-
project app node_modules/@vmz/vmz
|
|
79
|
+
project app node_modules/@vmz/vmz — full CLI
|
|
71
80
|
global npm/pnpm -g — only new/init/help/version
|
|
72
81
|
|
|
73
82
|
You are in global mode. Pin \`@vmz/vmz\` in the app so check/build/lsp
|
|
@@ -79,11 +88,11 @@ Usage:
|
|
|
79
88
|
vmz help Show this help
|
|
80
89
|
|
|
81
90
|
Project commands:
|
|
82
|
-
pnpm add -D @vmz/vmz
|
|
91
|
+
pnpm add @vmz/core && pnpm add -D @vmz/vmz
|
|
83
92
|
pnpm exec vmz check
|
|
84
93
|
# or: vmz new my-app && cd my-app && pnpm install
|
|
85
94
|
|
|
86
|
-
If a project \`node_modules/@vmz/vmz\`
|
|
95
|
+
If a project \`node_modules/@vmz/vmz\` exists, a global
|
|
87
96
|
\`vmz <cmd>\` re-execs that bin.
|
|
88
97
|
`);
|
|
89
98
|
}
|
|
@@ -101,6 +110,7 @@ Usage:
|
|
|
101
110
|
vmz test [path] [options] Native test discover / report
|
|
102
111
|
vmz document|docs <cmd> Project /documents domain
|
|
103
112
|
vmz application <cmd> Application Collection / Mount
|
|
113
|
+
vmz artifact <cmd> Release pack / publish / rollback / diff (A3)
|
|
104
114
|
vmz refactor <cmd> DX rename plans / apply
|
|
105
115
|
vmz explain [style] <target> DX causal explain (style Theme chain)
|
|
106
116
|
vmz lsp [root] [--out-dir] Language server (stdio; native CLI)
|
|
@@ -111,8 +121,10 @@ Usage:
|
|
|
111
121
|
Options:
|
|
112
122
|
--out-dir, -o <dir> Output directory (default: dist)
|
|
113
123
|
--release Release build (build only)
|
|
124
|
+
--profile <name> Delivery profile after build (web-static)
|
|
125
|
+
--origin <url> Site origin for web-static canonical/sitemap
|
|
114
126
|
--host <host> Listen host (default: 127.0.0.1)
|
|
115
|
-
--port <port> Listen port (
|
|
127
|
+
--port <port> Listen port (dev: omit = auto from 5173; set = lock)
|
|
116
128
|
--poll-ms <ms> Dev watch poll interval (default: 300)
|
|
117
129
|
--build Build before serve
|
|
118
130
|
--check Format check-only (format)
|
|
@@ -197,6 +209,10 @@ export async function runCli(argv, opts = {}) {
|
|
|
197
209
|
case 'applications':
|
|
198
210
|
case 'app':
|
|
199
211
|
return cmdApplication(rest);
|
|
212
|
+
case 'artifact':
|
|
213
|
+
case 'artifacts':
|
|
214
|
+
case 'release':
|
|
215
|
+
return cmdArtifact(rest);
|
|
200
216
|
case 'refactor':
|
|
201
217
|
return cmdRefactor(rest);
|
|
202
218
|
case 'explain':
|
|
@@ -278,7 +294,7 @@ function cmdCheck(args) {
|
|
|
278
294
|
* @param {import('./index.js').Workspace} ws
|
|
279
295
|
* @param {string} project
|
|
280
296
|
* @param {string} outDir
|
|
281
|
-
* @param { => Promise<number> | number} fn
|
|
297
|
+
* @param {() => Promise<number> | number} fn
|
|
282
298
|
*/
|
|
283
299
|
async function runWithPlugins(ws, project, outDir, fn) {
|
|
284
300
|
const { loadVmzConfig, applyPlugins } = await import('./plugin-host.js');
|
|
@@ -313,11 +329,39 @@ async function cmdBuild(args) {
|
|
|
313
329
|
});
|
|
314
330
|
if (code !== 0)
|
|
315
331
|
return code;
|
|
332
|
+
const localeEmit = emitLocaleRuntimeModules(project, outDir);
|
|
333
|
+
if (!localeEmit.ok || localeHasErrors({ diagnostics: localeEmit.diagnostics })) {
|
|
334
|
+
log.diagnostics(localeEmit.diagnostics ?? []);
|
|
335
|
+
log.error('locale runtime emit failed');
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
if (localeEmit.written.length) {
|
|
339
|
+
log.info(`locale runtime emit (${localeEmit.written.length} module(s))`);
|
|
340
|
+
}
|
|
316
341
|
if (projectHasDocuments(project)) {
|
|
317
342
|
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
318
343
|
if (!docs.ok)
|
|
319
344
|
return 1;
|
|
320
345
|
}
|
|
346
|
+
const cfg = await loadVmzConfig(project);
|
|
347
|
+
if (cfg.delivery) {
|
|
348
|
+
log.info(`site-delivery emit ${outDir}`);
|
|
349
|
+
const site = emitSiteDelivery(outDir, cfg.delivery, {
|
|
350
|
+
siteId: cfg.application?.id || undefined,
|
|
351
|
+
});
|
|
352
|
+
log.info(`site-delivery ok (digest=${String(site.contract.contractDigest).slice(0, 12)}…)`);
|
|
353
|
+
}
|
|
354
|
+
const profile = typeof args.profile === 'string' ? args.profile : '';
|
|
355
|
+
if (profile === 'web-static') {
|
|
356
|
+
const origin = typeof args.origin === 'string' ? args.origin : undefined;
|
|
357
|
+
log.info(`web-static emit ${outDir}`);
|
|
358
|
+
const result = await emitWebStatic(outDir, { origin });
|
|
359
|
+
log.info(`web-static ok (${result.htmlFiles.length} html, ${result.skipped.length} skipped, digest=${result.digest.slice(0, 12)}…)`);
|
|
360
|
+
}
|
|
361
|
+
else if (profile) {
|
|
362
|
+
log.error(`unknown build --profile ${profile} (supported: web-static)`);
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
321
365
|
return 0;
|
|
322
366
|
}
|
|
323
367
|
finally {
|
|
@@ -391,7 +435,22 @@ async function cmdDev(args) {
|
|
|
391
435
|
outDir: typeof args['out-dir'] === 'string' ? args['out-dir'] : undefined,
|
|
392
436
|
});
|
|
393
437
|
const host = typeof args.host === 'string' ? args.host : '127.0.0.1';
|
|
394
|
-
const
|
|
438
|
+
const portLocked = Object.prototype.hasOwnProperty.call(args, 'port');
|
|
439
|
+
let port;
|
|
440
|
+
if (portLocked) {
|
|
441
|
+
port = Number(args.port);
|
|
442
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
443
|
+
log.error(`invalid --port ${String(args.port)}`);
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
const preferred = 5173;
|
|
449
|
+
port = await findAvailablePort(host, preferred);
|
|
450
|
+
if (port !== preferred) {
|
|
451
|
+
log.info(`port ${preferred} busy → using ${port}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
395
454
|
const pollMs = Number(args['poll-ms'] ?? 300);
|
|
396
455
|
const ac = new AbortController();
|
|
397
456
|
const onSig = () => {
|