@vmz/vmz 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/build-assemble.d.ts +52 -0
- package/dist/build-assemble.js +191 -0
- package/dist/cdn-policy.d.ts +196 -0
- package/dist/cdn-policy.js +443 -0
- package/dist/cli.js +152 -11
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/delivery-profile.d.ts +74 -0
- package/dist/delivery-profile.js +279 -0
- package/dist/dev-session.js +49 -13
- package/dist/document-build.js +9 -4
- package/dist/document-designs.js +30 -2
- package/dist/document-enrich.js +8 -0
- package/dist/embedded-packaging.d.ts +22 -0
- package/dist/embedded-packaging.js +113 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.js +35 -15
- package/dist/invocation.d.ts +8 -31
- package/dist/invocation.js +12 -33
- package/dist/locale-check.d.ts +16 -0
- package/dist/locale-check.js +131 -3
- package/dist/locale-cmd.js +2 -2
- package/dist/locale-route-emit.d.ts +34 -0
- package/dist/locale-route-emit.js +134 -0
- package/dist/locale-router.d.ts +20 -0
- package/dist/locale-router.js +68 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +11 -3
- package/dist/pack.d.ts +40 -0
- package/dist/pack.js +108 -0
- package/dist/plugin-host.d.ts +10 -1
- package/dist/plugin-host.js +19 -2
- package/dist/port.d.ts +10 -0
- package/dist/port.js +46 -0
- package/dist/production-observability.d.ts +286 -0
- package/dist/production-observability.js +469 -0
- package/dist/production-test-pack.d.ts +144 -0
- package/dist/production-test-pack.js +447 -0
- package/dist/release-cmd.d.ts +8 -0
- package/dist/release-cmd.js +126 -0
- package/dist/release-pack.d.ts +96 -0
- package/dist/release-pack.js +346 -0
- package/dist/server-artifact.d.ts +140 -0
- package/dist/server-artifact.js +205 -0
- package/dist/server-language-backend.d.ts +89 -0
- package/dist/server-language-backend.js +121 -0
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +145 -0
- package/dist/static-emit.js +577 -0
- package/package.json +13 -13
|
@@ -0,0 +1,443 @@
|
|
|
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
|
+
import { assertLocaleCacheKey, localeAwareCacheKey } from './locale-router.js';
|
|
11
|
+
export const CDN_POLICY_MANIFEST_SCHEMA = 'vmz.cdn.policy_manifest.v0';
|
|
12
|
+
export const CDN_ADAPTER_PROJECTION_SCHEMA = 'vmz.cdn.adapter_projection.v0';
|
|
13
|
+
/** HTML: revalidate. Hashed/static assets: long immutable. */
|
|
14
|
+
export const CACHE_HTML = 'public, max-age=0, must-revalidate';
|
|
15
|
+
export const CACHE_ASSET_IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
16
|
+
export const CACHE_META = 'public, max-age=3600';
|
|
17
|
+
/**
|
|
18
|
+
* Build CDNPolicyManifest from StaticDeliveryManifest (+ optional redirects / locale artifact).
|
|
19
|
+
* Locale-prefixed HTML gets LocaleId-encoded cache keys; Accept-Language must not steal body.
|
|
20
|
+
* @param {Record<string, any>} staticManifest
|
|
21
|
+
* @param {{
|
|
22
|
+
* redirects?: Array<{ from: string, to: string, status?: number, reason?: string }>,
|
|
23
|
+
* localeArtifact?: Record<string, any> | null,
|
|
24
|
+
* }} [opts]
|
|
25
|
+
*/
|
|
26
|
+
export function buildCdnPolicyManifest(staticManifest, opts = {}) {
|
|
27
|
+
const origin = String(staticManifest.origin || '');
|
|
28
|
+
const localeArt = opts.localeArtifact || null;
|
|
29
|
+
const localeRedirects = buildOmitPrefixRedirects(staticManifest, localeArt);
|
|
30
|
+
const redirects = [
|
|
31
|
+
{ from: '/home', to: '/', status: 301, reason: 'canonical-alias' },
|
|
32
|
+
...localeRedirects,
|
|
33
|
+
...(opts.redirects || []),
|
|
34
|
+
];
|
|
35
|
+
const headers = [
|
|
36
|
+
{ match: '**/*.html', headers: { 'cache-control': CACHE_HTML } },
|
|
37
|
+
{
|
|
38
|
+
match: '**/404.html',
|
|
39
|
+
headers: { 'cache-control': CACHE_HTML, 'x-robots-tag': 'noindex, nofollow' },
|
|
40
|
+
},
|
|
41
|
+
// Content-addressed immutable objects (A3 assets/<hash>).
|
|
42
|
+
{ match: '**/assets/**', headers: { 'cache-control': CACHE_ASSET_IMMUTABLE } },
|
|
43
|
+
{ match: '**/*.{js,css,mjs}', headers: { 'cache-control': CACHE_ASSET_IMMUTABLE } },
|
|
44
|
+
{ match: '**/sitemap.xml', headers: { 'cache-control': CACHE_META } },
|
|
45
|
+
{ match: '**/robots.txt', headers: { 'cache-control': CACHE_META } },
|
|
46
|
+
];
|
|
47
|
+
const errorDocuments = Array.isArray(staticManifest.errorDocuments) ? staticManifest.errorDocuments : [{ status: 404, path: '404.html' }];
|
|
48
|
+
/** @type {any[]} */
|
|
49
|
+
const routes = [];
|
|
50
|
+
/** @type {any[]} */
|
|
51
|
+
const cacheKeyDiagnostics = [];
|
|
52
|
+
for (const r of staticManifest.routes || []) {
|
|
53
|
+
const localeId = r.localeId || null;
|
|
54
|
+
const alternates = Array.isArray(r.seo?.alternates) ? r.seo.alternates : [];
|
|
55
|
+
const cacheKey = localeId
|
|
56
|
+
? localeAwareCacheKey({ routeId: String(r.routeId), localeId: String(localeId), path: String(r.path) })
|
|
57
|
+
: `route=${r.routeId}|path=${r.path}`;
|
|
58
|
+
// When LocaleId is bound, prove the key would still be safe even if Vary: Accept-Language were set.
|
|
59
|
+
if (localeId) {
|
|
60
|
+
const assert = assertLocaleCacheKey({
|
|
61
|
+
cacheKey,
|
|
62
|
+
varyAcceptLanguage: true,
|
|
63
|
+
localeId: String(localeId),
|
|
64
|
+
});
|
|
65
|
+
if (!assert.ok) {
|
|
66
|
+
cacheKeyDiagnostics.push(...(assert.diagnostics || []));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
routes.push({
|
|
70
|
+
routeId: r.routeId,
|
|
71
|
+
path: r.path,
|
|
72
|
+
htmlPath: r.htmlPath,
|
|
73
|
+
canonical: r.seo?.canonical || null,
|
|
74
|
+
localeId,
|
|
75
|
+
cacheKey,
|
|
76
|
+
varyAcceptLanguage: false,
|
|
77
|
+
hreflang: alternates.map((a) => ({
|
|
78
|
+
hreflang: a.hreflang,
|
|
79
|
+
href: a.href,
|
|
80
|
+
localeId: a.localeId || null,
|
|
81
|
+
})),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (cacheKeyDiagnostics.length) {
|
|
85
|
+
const msg = cacheKeyDiagnostics.map((d) => d.message || d.code).join('; ');
|
|
86
|
+
throw new Error(`buildCdnPolicyManifest: locale cache key contract failed: ${msg}`);
|
|
87
|
+
}
|
|
88
|
+
const localeRoutes = routes.filter((r) => r.localeId);
|
|
89
|
+
const body = {
|
|
90
|
+
schema: CDN_POLICY_MANIFEST_SCHEMA,
|
|
91
|
+
applicationId: staticManifest.applicationId || null,
|
|
92
|
+
deliveryProfile: 'web-static',
|
|
93
|
+
origin,
|
|
94
|
+
spaFallback: false,
|
|
95
|
+
staticManifestDigest: staticManifest.manifestDigest || null,
|
|
96
|
+
redirects,
|
|
97
|
+
headers,
|
|
98
|
+
errorDocuments,
|
|
99
|
+
routes,
|
|
100
|
+
localeCache: {
|
|
101
|
+
strategy: 'path-locale',
|
|
102
|
+
varyAcceptLanguage: false,
|
|
103
|
+
defaultLocale: localeArt?.defaultLocale || null,
|
|
104
|
+
routeCount: localeRoutes.length,
|
|
105
|
+
locales: [...new Set(localeRoutes.map((r) => r.localeId))],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
body.policyDigest = sha256Hex(canonicalJson(body));
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Omit-prefix: /{defaultLocale}/… → canonical unprefixed path (CDN redirect, not Accept-Language).
|
|
113
|
+
* @param {Record<string, any>} staticManifest
|
|
114
|
+
* @param {Record<string, any> | null} localeArt
|
|
115
|
+
*/
|
|
116
|
+
function buildOmitPrefixRedirects(staticManifest, localeArt) {
|
|
117
|
+
const routing = localeArt?.routing || {};
|
|
118
|
+
const defaultLocale = localeArt?.defaultLocale || routing.defaultLocale;
|
|
119
|
+
if (!defaultLocale || routing.defaultPrefix !== 'omit')
|
|
120
|
+
return [];
|
|
121
|
+
/** @type {Array<{ from: string, to: string, status: number, reason: string }>} */
|
|
122
|
+
const out = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
for (const r of staticManifest.routes || []) {
|
|
125
|
+
if (r.localeId !== defaultLocale)
|
|
126
|
+
continue;
|
|
127
|
+
const canonical = String(r.path || '/');
|
|
128
|
+
const from = canonical === '/' ? `/${defaultLocale}` : `/${defaultLocale}${canonical.startsWith('/') ? canonical : `/${canonical}`}`;
|
|
129
|
+
if (seen.has(from))
|
|
130
|
+
continue;
|
|
131
|
+
seen.add(from);
|
|
132
|
+
out.push({
|
|
133
|
+
from,
|
|
134
|
+
to: canonical,
|
|
135
|
+
status: 301,
|
|
136
|
+
reason: 'locale-omit-prefix-default',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Write CDN policy + adapter projections under dist/_vmz.
|
|
143
|
+
* @param {string} distDir
|
|
144
|
+
* @param {Record<string, any>} staticManifest
|
|
145
|
+
* @param {{ redirects?: Array<{ from: string, to: string, status?: number, reason?: string }> }} [opts]
|
|
146
|
+
*/
|
|
147
|
+
export function emitCdnPolicy(distDir, staticManifest, opts = {}) {
|
|
148
|
+
const localeArtifact = loadLocaleArtifact(distDir);
|
|
149
|
+
const policy = buildCdnPolicyManifest(staticManifest, { ...opts, localeArtifact });
|
|
150
|
+
const vmzDir = path.join(distDir, '_vmz');
|
|
151
|
+
fs.mkdirSync(vmzDir, { recursive: true });
|
|
152
|
+
fs.writeFileSync(path.join(vmzDir, 'cdn-policy-manifest.json'), `${JSON.stringify(policy, null, 2)}\n`, 'utf8');
|
|
153
|
+
const local = projectCdnAdapter(policy, 'local-static');
|
|
154
|
+
const netlify = projectCdnAdapter(policy, 'netlify');
|
|
155
|
+
const adaptersDir = path.join(vmzDir, 'adapters');
|
|
156
|
+
fs.mkdirSync(path.join(adaptersDir, 'local-static'), { recursive: true });
|
|
157
|
+
fs.mkdirSync(path.join(adaptersDir, 'netlify'), { recursive: true });
|
|
158
|
+
fs.writeFileSync(path.join(adaptersDir, 'local-static', 'projection.json'), `${JSON.stringify(local, null, 2)}\n`, 'utf8');
|
|
159
|
+
fs.writeFileSync(path.join(adaptersDir, 'netlify', 'projection.json'), `${JSON.stringify(netlify, null, 2)}\n`, 'utf8');
|
|
160
|
+
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_headers'), String(netlify.files['_headers'] || ''), 'utf8');
|
|
161
|
+
fs.writeFileSync(path.join(adaptersDir, 'netlify', '_redirects'), String(netlify.files['_redirects'] || ''), 'utf8');
|
|
162
|
+
return { policy, adapters: { 'local-static': local, netlify } };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} distDir
|
|
166
|
+
*/
|
|
167
|
+
function loadLocaleArtifact(distDir) {
|
|
168
|
+
const p = path.join(distDir, '_vmz', 'locale-route-realization.json');
|
|
169
|
+
if (!fs.existsSync(p))
|
|
170
|
+
return null;
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Project vendor-neutral policy to an adapter. Must preserve redirect targets and forbid SPA fallback.
|
|
180
|
+
* @param {Record<string, any>} policy
|
|
181
|
+
* @param {'local-static' | 'netlify'} adapterId
|
|
182
|
+
*/
|
|
183
|
+
export function projectCdnAdapter(policy, adapterId) {
|
|
184
|
+
if (policy.spaFallback) {
|
|
185
|
+
throw new Error('projectCdnAdapter: spaFallback=true is forbidden');
|
|
186
|
+
}
|
|
187
|
+
if (adapterId === 'local-static') {
|
|
188
|
+
return {
|
|
189
|
+
schema: CDN_ADAPTER_PROJECTION_SCHEMA,
|
|
190
|
+
adapterId: 'local-static',
|
|
191
|
+
policyDigest: policy.policyDigest,
|
|
192
|
+
host: 'vmz-local-static',
|
|
193
|
+
spaFallback: false,
|
|
194
|
+
redirects: policy.redirects,
|
|
195
|
+
headers: policy.headers,
|
|
196
|
+
errorDocuments: policy.errorDocuments,
|
|
197
|
+
files: {},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (adapterId === 'netlify') {
|
|
201
|
+
const headerLines = [];
|
|
202
|
+
for (const rule of policy.headers || []) {
|
|
203
|
+
const glob = netlifyGlob(rule.match);
|
|
204
|
+
headerLines.push(glob);
|
|
205
|
+
for (const [k, v] of Object.entries(rule.headers || {})) {
|
|
206
|
+
headerLines.push(` ${headerCase(k)}: ${v}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const redirectLines = [];
|
|
210
|
+
for (const r of policy.redirects || []) {
|
|
211
|
+
redirectLines.push(`${r.from} ${r.to} ${Number(r.status) || 301}`);
|
|
212
|
+
}
|
|
213
|
+
// Explicit only — never `/* /index.html 200`
|
|
214
|
+
const redirectsText = redirectLines.join('\n') + (redirectLines.length ? '\n' : '');
|
|
215
|
+
if (/\*\s+\/index\.html/.test(redirectsText)) {
|
|
216
|
+
throw new Error('netlify adapter refused SPA fallback redirect');
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
schema: CDN_ADAPTER_PROJECTION_SCHEMA,
|
|
220
|
+
adapterId: 'netlify',
|
|
221
|
+
policyDigest: policy.policyDigest,
|
|
222
|
+
host: 'netlify',
|
|
223
|
+
spaFallback: false,
|
|
224
|
+
redirects: policy.redirects,
|
|
225
|
+
headers: policy.headers,
|
|
226
|
+
errorDocuments: policy.errorDocuments,
|
|
227
|
+
files: {
|
|
228
|
+
_headers: headerLines.join('\n') + '\n',
|
|
229
|
+
_redirects: redirectsText,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`unknown CDN adapter ${adapterId}`);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Local static host that applies CDNPolicyManifest (redirects, cache headers, 404 doc).
|
|
237
|
+
* @param {string} distDir
|
|
238
|
+
* @param {Record<string, any>} policy
|
|
239
|
+
* @param {{ host?: string, port?: number }} [opts]
|
|
240
|
+
* @returns {Promise<{ host: string, port: number, baseUrl: string, close: () => Promise<void> }>}
|
|
241
|
+
*/
|
|
242
|
+
export function listenLocalStaticHost(distDir, policy, opts = {}) {
|
|
243
|
+
const host = opts.host || '127.0.0.1';
|
|
244
|
+
const port = Number(opts.port || 0);
|
|
245
|
+
const handler = createLocalStaticHandler(distDir, policy);
|
|
246
|
+
const server = http.createServer(handler);
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
server.listen(port, host, () => {
|
|
249
|
+
const addr = server.address();
|
|
250
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
251
|
+
resolve({
|
|
252
|
+
host,
|
|
253
|
+
port: actualPort,
|
|
254
|
+
baseUrl: `http://${host}:${actualPort}`,
|
|
255
|
+
close: () => new Promise((res, rej) => {
|
|
256
|
+
server.close((err) => (err ? rej(err) : res()));
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
server.on('error', reject);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* @param {string} distDir
|
|
265
|
+
* @param {Record<string, any>} policy
|
|
266
|
+
*/
|
|
267
|
+
export function createLocalStaticHandler(distDir, policy) {
|
|
268
|
+
const root = path.resolve(distDir);
|
|
269
|
+
return (req, res) => {
|
|
270
|
+
try {
|
|
271
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
272
|
+
let pathname = decodeURIComponent(url.pathname || '/');
|
|
273
|
+
const redirect = matchRedirect(policy.redirects || [], pathname);
|
|
274
|
+
if (redirect) {
|
|
275
|
+
const status = Number(redirect.status) || 301;
|
|
276
|
+
const headers = applyHeaderRules(policy.headers || [], pathname, {
|
|
277
|
+
Location: redirect.to,
|
|
278
|
+
});
|
|
279
|
+
res.writeHead(status, headers);
|
|
280
|
+
res.end();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
let rel = pathname;
|
|
284
|
+
if (rel.endsWith('/'))
|
|
285
|
+
rel += 'index.html';
|
|
286
|
+
if (rel === '/')
|
|
287
|
+
rel = '/index.html';
|
|
288
|
+
const file = safeJoin(root, rel.replace(/^\//, ''));
|
|
289
|
+
if (file && fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
290
|
+
const body = fs.readFileSync(file);
|
|
291
|
+
const type = contentType(file);
|
|
292
|
+
const headers = applyHeaderRules(policy.headers || [], rel, {
|
|
293
|
+
'content-type': type,
|
|
294
|
+
});
|
|
295
|
+
res.writeHead(200, headers);
|
|
296
|
+
res.end(body);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const errDoc = (policy.errorDocuments || []).find((e) => Number(e.status) === 404);
|
|
300
|
+
const errPath = errDoc?.path || '404.html';
|
|
301
|
+
const abs404 = path.join(root, errPath);
|
|
302
|
+
if (fs.existsSync(abs404)) {
|
|
303
|
+
const body = fs.readFileSync(abs404);
|
|
304
|
+
const headers = applyHeaderRules(policy.headers || [], `/${errPath}`, {
|
|
305
|
+
'content-type': 'text/html; charset=utf-8',
|
|
306
|
+
});
|
|
307
|
+
res.writeHead(404, headers);
|
|
308
|
+
res.end(body);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
312
|
+
res.end('not found');
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
|
|
316
|
+
res.end(err instanceof Error ? err.message : String(err));
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* @param {Array<{ from: string, to: string, status?: number }>} redirects
|
|
322
|
+
* @param {string} pathname
|
|
323
|
+
*/
|
|
324
|
+
function matchRedirect(redirects, pathname) {
|
|
325
|
+
const p = pathname.replace(/\/+$/, '') || '/';
|
|
326
|
+
for (const r of redirects) {
|
|
327
|
+
const from = String(r.from || '').replace(/\/+$/, '') || '/';
|
|
328
|
+
if (from === p || from === pathname)
|
|
329
|
+
return r;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* @param {Array<{ match: string, headers: Record<string, string> }>} rules
|
|
335
|
+
* @param {string} pathname
|
|
336
|
+
* @param {Record<string, string>} base
|
|
337
|
+
*/
|
|
338
|
+
function applyHeaderRules(rules, pathname, base = {}) {
|
|
339
|
+
/** @type {Record<string, string>} */
|
|
340
|
+
const out = { ...base };
|
|
341
|
+
for (const rule of rules) {
|
|
342
|
+
if (matchGlob(rule.match, pathname)) {
|
|
343
|
+
Object.assign(out, rule.headers || {});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Minimal glob matcher for CDN header rules (html / js|css|mjs / exact / prefix).
|
|
350
|
+
* @param {string} pattern
|
|
351
|
+
* @param {string} pathname
|
|
352
|
+
*/
|
|
353
|
+
export function matchGlob(pattern, pathname) {
|
|
354
|
+
const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
355
|
+
const pat = String(pattern || '');
|
|
356
|
+
if (pat === '**/*.html')
|
|
357
|
+
return p.endsWith('.html');
|
|
358
|
+
if (pat === '**/404.html')
|
|
359
|
+
return p === '/404.html' || p.endsWith('/404.html');
|
|
360
|
+
if (pat === '**/assets/**')
|
|
361
|
+
return p === '/assets' || p.startsWith('/assets/');
|
|
362
|
+
if (pat === '**/*.{js,css,mjs}')
|
|
363
|
+
return /\.(js|css|mjs)$/.test(p);
|
|
364
|
+
if (pat === '**/sitemap.xml')
|
|
365
|
+
return p.endsWith('/sitemap.xml') || p === '/sitemap.xml';
|
|
366
|
+
if (pat === '**/robots.txt')
|
|
367
|
+
return p.endsWith('/robots.txt') || p === '/robots.txt';
|
|
368
|
+
if (pat.endsWith('/**')) {
|
|
369
|
+
const prefix = pat.slice(0, -3);
|
|
370
|
+
// Only treat as path prefix when pattern is absolute-ish (starts with / or bare segment).
|
|
371
|
+
if (prefix.startsWith('/')) {
|
|
372
|
+
return p === prefix || p.startsWith(prefix + '/');
|
|
373
|
+
}
|
|
374
|
+
if (!prefix.includes('*')) {
|
|
375
|
+
return p === `/${prefix}` || p.startsWith(`/${prefix}/`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return p === pat || p === `/${pat.replace(/^\//, '')}`;
|
|
379
|
+
}
|
|
380
|
+
function netlifyGlob(match) {
|
|
381
|
+
if (match === '**/*.html')
|
|
382
|
+
return '/*.html';
|
|
383
|
+
if (match === '**/404.html')
|
|
384
|
+
return '/404.html';
|
|
385
|
+
if (match === '**/assets/**')
|
|
386
|
+
return '/assets/*';
|
|
387
|
+
if (match === '**/*.{js,css,mjs}')
|
|
388
|
+
return '/*.{js,css,mjs}';
|
|
389
|
+
if (match === '**/sitemap.xml')
|
|
390
|
+
return '/sitemap.xml';
|
|
391
|
+
if (match === '**/robots.txt')
|
|
392
|
+
return '/robots.txt';
|
|
393
|
+
return match.startsWith('/') ? match : `/${match}`;
|
|
394
|
+
}
|
|
395
|
+
function headerCase(name) {
|
|
396
|
+
return String(name)
|
|
397
|
+
.split('-')
|
|
398
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
399
|
+
.join('-');
|
|
400
|
+
}
|
|
401
|
+
function contentType(file) {
|
|
402
|
+
if (file.endsWith('.html'))
|
|
403
|
+
return 'text/html; charset=utf-8';
|
|
404
|
+
if (file.endsWith('.js') || file.endsWith('.mjs'))
|
|
405
|
+
return 'text/javascript; charset=utf-8';
|
|
406
|
+
if (file.endsWith('.css'))
|
|
407
|
+
return 'text/css; charset=utf-8';
|
|
408
|
+
if (file.endsWith('.xml'))
|
|
409
|
+
return 'application/xml';
|
|
410
|
+
if (file.endsWith('.txt'))
|
|
411
|
+
return 'text/plain; charset=utf-8';
|
|
412
|
+
if (file.endsWith('.json'))
|
|
413
|
+
return 'application/json; charset=utf-8';
|
|
414
|
+
return 'application/octet-stream';
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* @param {string} root
|
|
418
|
+
* @param {string} rel
|
|
419
|
+
*/
|
|
420
|
+
function safeJoin(root, rel) {
|
|
421
|
+
const full = path.resolve(root, rel);
|
|
422
|
+
const normRoot = path.resolve(root);
|
|
423
|
+
if (full !== normRoot && !full.startsWith(normRoot + path.sep))
|
|
424
|
+
return null;
|
|
425
|
+
return full;
|
|
426
|
+
}
|
|
427
|
+
function sha256Hex(data) {
|
|
428
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
429
|
+
}
|
|
430
|
+
function canonicalJson(value) {
|
|
431
|
+
return JSON.stringify(sortKeys(value));
|
|
432
|
+
}
|
|
433
|
+
function sortKeys(value) {
|
|
434
|
+
if (Array.isArray(value))
|
|
435
|
+
return value.map(sortKeys);
|
|
436
|
+
if (value && typeof value === 'object') {
|
|
437
|
+
const out = {};
|
|
438
|
+
for (const k of Object.keys(value).sort())
|
|
439
|
+
out[k] = sortKeys(value[k]);
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
return value;
|
|
443
|
+
}
|