@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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3-static: emit per-route HTML + 404 + SEO head + StaticDeliveryManifest.
|
|
3
|
+
* Reuses the same Direct SSR path as serve-host (no second SSG runtime).
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { emitCdnPolicy } from './cdn-policy.js';
|
|
11
|
+
import { emitContentAddressedAssets } from './content-addressed-assets.js';
|
|
12
|
+
export const STATIC_DELIVERY_MANIFEST_SCHEMA = 'vmz.static.delivery_manifest.v0';
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} distDir
|
|
15
|
+
* @param {{
|
|
16
|
+
* origin?: string,
|
|
17
|
+
* applicationId?: string,
|
|
18
|
+
* staticParams?: Record<string, Array<Record<string, string>>>,
|
|
19
|
+
* }} [opts]
|
|
20
|
+
*/
|
|
21
|
+
export async function emitWebStatic(distDir, opts = {}) {
|
|
22
|
+
const origin = String(opts.origin || process.env.VMZ_SITE_ORIGIN || 'https://example.test').replace(/\/$/, '');
|
|
23
|
+
const applicationId = opts.applicationId || path.basename(path.dirname(distDir));
|
|
24
|
+
const domPath = path.join(distDir, 'vmz-dom.js');
|
|
25
|
+
if (!fs.existsSync(domPath)) {
|
|
26
|
+
throw new Error(`emitWebStatic: missing ${domPath} — run vmz build first`);
|
|
27
|
+
}
|
|
28
|
+
const { renderToString, renderToStream } = await import(pathToFileURL(domPath).href);
|
|
29
|
+
const pageCatalog = listPageClientFiles(distDir);
|
|
30
|
+
/** @type {Array<{
|
|
31
|
+
* routeId: string,
|
|
32
|
+
* path: string,
|
|
33
|
+
* chunkId: string,
|
|
34
|
+
* htmlPath: string,
|
|
35
|
+
* classification: string,
|
|
36
|
+
* title: string,
|
|
37
|
+
* description: string,
|
|
38
|
+
* canonical: string,
|
|
39
|
+
* robots: string,
|
|
40
|
+
* }>} */
|
|
41
|
+
const generations = [];
|
|
42
|
+
/** @type {Array<{ routeId: string, path: string, chunkId: string, classification: string, reason: string }>} */
|
|
43
|
+
const skipped = [];
|
|
44
|
+
for (const page of pageCatalog) {
|
|
45
|
+
const pattern = patternFromSegs(page.segs);
|
|
46
|
+
const routeId = guessRouteId(distDir, page.chunkId);
|
|
47
|
+
if (page.segs.some((s) => s.kind === 'param' || s.kind === 'catch')) {
|
|
48
|
+
skipped.push({
|
|
49
|
+
routeId,
|
|
50
|
+
path: pattern,
|
|
51
|
+
chunkId: page.chunkId,
|
|
52
|
+
classification: 'ServerRequired',
|
|
53
|
+
reason: 'dynamic params require explicit StaticRouteSource (not in this thin slice)',
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const Page = await loadCtor(distDir, page.chunkId);
|
|
58
|
+
if (!Page) {
|
|
59
|
+
skipped.push({
|
|
60
|
+
routeId,
|
|
61
|
+
path: pattern,
|
|
62
|
+
chunkId: page.chunkId,
|
|
63
|
+
classification: 'UnsupportedForStatic',
|
|
64
|
+
reason: 'missing page ctor',
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const params = {};
|
|
69
|
+
if (typeof Page.access === 'function') {
|
|
70
|
+
const access = await Page.access({ params, pathname: pattern, chunkId: page.chunkId, method: 'GET' });
|
|
71
|
+
const kind = access && typeof access === 'object' ? String(access.kind || 'allow') : 'allow';
|
|
72
|
+
if (kind !== 'allow') {
|
|
73
|
+
skipped.push({
|
|
74
|
+
routeId,
|
|
75
|
+
path: pattern,
|
|
76
|
+
chunkId: page.chunkId,
|
|
77
|
+
classification: 'ServerRequired',
|
|
78
|
+
reason: `access result ${kind} is request-bound`,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let props = { ...params };
|
|
84
|
+
if (typeof Page.load === 'function') {
|
|
85
|
+
const loaded = await Page.load({
|
|
86
|
+
params,
|
|
87
|
+
pathname: pattern,
|
|
88
|
+
chunkId: page.chunkId,
|
|
89
|
+
searchParams: new URLSearchParams(),
|
|
90
|
+
});
|
|
91
|
+
if (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) {
|
|
92
|
+
props = { ...props, ...loaded };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const meta = await resolvePageMeta(Page, { params, props, pathname: pattern, origin });
|
|
96
|
+
const layoutChain = resolveLayoutChain(distDir, page.chunkId);
|
|
97
|
+
let bodyHtml = '';
|
|
98
|
+
for await (const chunk of renderToStream(Page, props, {})) {
|
|
99
|
+
bodyHtml += chunk;
|
|
100
|
+
}
|
|
101
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
102
|
+
const Layout = await loadCtor(distDir, layoutChain[i]);
|
|
103
|
+
if (!Layout)
|
|
104
|
+
continue;
|
|
105
|
+
bodyHtml = await renderToString(Layout, {}, { slotHtml: bodyHtml });
|
|
106
|
+
}
|
|
107
|
+
const htmlPath = htmlPathForRoute(pattern);
|
|
108
|
+
const absHtml = path.join(distDir, htmlPath);
|
|
109
|
+
fs.mkdirSync(path.dirname(absHtml), { recursive: true });
|
|
110
|
+
const html = wrapDocument({
|
|
111
|
+
bodyHtml,
|
|
112
|
+
chunkId: page.chunkId,
|
|
113
|
+
layoutChain,
|
|
114
|
+
props,
|
|
115
|
+
meta,
|
|
116
|
+
cssEntry: readCssEntry(distDir),
|
|
117
|
+
});
|
|
118
|
+
fs.writeFileSync(absHtml, html, 'utf8');
|
|
119
|
+
generations.push({
|
|
120
|
+
routeId,
|
|
121
|
+
path: pattern,
|
|
122
|
+
chunkId: page.chunkId,
|
|
123
|
+
htmlPath: htmlPath.replaceAll('\\', '/'),
|
|
124
|
+
classification: 'Static',
|
|
125
|
+
title: meta.title,
|
|
126
|
+
description: meta.description,
|
|
127
|
+
canonical: meta.canonical,
|
|
128
|
+
robots: meta.robots,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const notFoundHtml = wrapDocument({
|
|
132
|
+
bodyHtml: '<main><h1>Not Found</h1><p>route-static-404</p></main>',
|
|
133
|
+
chunkId: '',
|
|
134
|
+
layoutChain: [],
|
|
135
|
+
props: {},
|
|
136
|
+
meta: {
|
|
137
|
+
title: 'Not Found',
|
|
138
|
+
description: 'Page not found',
|
|
139
|
+
canonical: `${origin}/404`,
|
|
140
|
+
robots: 'noindex,nofollow',
|
|
141
|
+
lang: 'en',
|
|
142
|
+
},
|
|
143
|
+
cssEntry: readCssEntry(distDir),
|
|
144
|
+
isErrorDocument: true,
|
|
145
|
+
});
|
|
146
|
+
fs.writeFileSync(path.join(distDir, '404.html'), notFoundHtml, 'utf8');
|
|
147
|
+
const sitemap = buildSitemap(origin, generations);
|
|
148
|
+
fs.writeFileSync(path.join(distDir, 'sitemap.xml'), sitemap, 'utf8');
|
|
149
|
+
const robots = `User-agent: *\nAllow: /\nDisallow: /404\nSitemap: ${origin}/sitemap.xml\n`;
|
|
150
|
+
fs.writeFileSync(path.join(distDir, 'robots.txt'), robots, 'utf8');
|
|
151
|
+
// Hard rule: no SPA fallback shim in artifact.
|
|
152
|
+
for (const bad of ['_redirects', 'vercel.json', 'netlify.toml']) {
|
|
153
|
+
const p = path.join(distDir, bad);
|
|
154
|
+
if (fs.existsSync(p)) {
|
|
155
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
156
|
+
if (/\/\*|\bspa\b|index\.html/i.test(text) && /fallback|rewrite|redirects/i.test(text)) {
|
|
157
|
+
throw new Error(`emitWebStatic: forbidden SPA fallback config present: ${bad}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const vmzDir = path.join(distDir, '_vmz');
|
|
162
|
+
fs.mkdirSync(vmzDir, { recursive: true });
|
|
163
|
+
const manifest = {
|
|
164
|
+
schema: STATIC_DELIVERY_MANIFEST_SCHEMA,
|
|
165
|
+
applicationId,
|
|
166
|
+
deliveryProfile: 'web-static',
|
|
167
|
+
origin,
|
|
168
|
+
generatedAt: new Date().toISOString(),
|
|
169
|
+
spaFallback: false,
|
|
170
|
+
errorDocuments: [{ status: 404, path: '404.html' }],
|
|
171
|
+
routes: generations.map((g) => ({
|
|
172
|
+
routeId: g.routeId,
|
|
173
|
+
path: g.path,
|
|
174
|
+
chunkId: g.chunkId,
|
|
175
|
+
htmlPath: g.htmlPath,
|
|
176
|
+
classification: g.classification,
|
|
177
|
+
seo: {
|
|
178
|
+
title: g.title,
|
|
179
|
+
description: g.description,
|
|
180
|
+
canonical: g.canonical,
|
|
181
|
+
robots: g.robots,
|
|
182
|
+
},
|
|
183
|
+
})),
|
|
184
|
+
skipped,
|
|
185
|
+
seoArtifacts: {
|
|
186
|
+
sitemap: 'sitemap.xml',
|
|
187
|
+
robots: 'robots.txt',
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
const digest = sha256Hex(canonicalJson(manifest));
|
|
191
|
+
manifest.manifestDigest = digest;
|
|
192
|
+
fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
193
|
+
const assets = emitContentAddressedAssets(distDir);
|
|
194
|
+
manifest.contentAddressedAssets = {
|
|
195
|
+
schema: assets.manifest.schema,
|
|
196
|
+
manifestDigest: assets.manifest.manifestDigest,
|
|
197
|
+
objectCount: assets.manifest.objectCount,
|
|
198
|
+
layout: assets.manifest.layout,
|
|
199
|
+
};
|
|
200
|
+
// Re-stamp static manifest after linking asset digest (HTML already rewritten on disk).
|
|
201
|
+
delete manifest.manifestDigest;
|
|
202
|
+
manifest.manifestDigest = sha256Hex(canonicalJson(manifest));
|
|
203
|
+
fs.writeFileSync(path.join(vmzDir, 'static-delivery-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
204
|
+
const cdn = emitCdnPolicy(distDir, manifest);
|
|
205
|
+
return {
|
|
206
|
+
manifest,
|
|
207
|
+
htmlFiles: generations.map((g) => g.htmlPath),
|
|
208
|
+
skipped,
|
|
209
|
+
digest: manifest.manifestDigest,
|
|
210
|
+
assets: assets.manifest,
|
|
211
|
+
cdnPolicy: cdn.policy,
|
|
212
|
+
cdnAdapters: cdn.adapters,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* @param {string} data
|
|
217
|
+
*/
|
|
218
|
+
function sha256Hex(data) {
|
|
219
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* @param {unknown} value
|
|
223
|
+
*/
|
|
224
|
+
function canonicalJson(value) {
|
|
225
|
+
return JSON.stringify(sortKeys(value));
|
|
226
|
+
}
|
|
227
|
+
function sortKeys(value) {
|
|
228
|
+
if (Array.isArray(value))
|
|
229
|
+
return value.map(sortKeys);
|
|
230
|
+
if (value && typeof value === 'object') {
|
|
231
|
+
/** @type {Record<string, unknown>} */
|
|
232
|
+
const out = {};
|
|
233
|
+
for (const k of Object.keys(value).sort())
|
|
234
|
+
out[k] = sortKeys(value[k]);
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @param {string} distDir
|
|
241
|
+
*/
|
|
242
|
+
function listPageClientFiles(distDir) {
|
|
243
|
+
const root = path.join(distDir, 'pages');
|
|
244
|
+
/** @type {Array<{ chunkId: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
245
|
+
const out = [];
|
|
246
|
+
function walk(abs, relParts) {
|
|
247
|
+
let ents;
|
|
248
|
+
try {
|
|
249
|
+
ents = fs.readdirSync(abs, { withFileTypes: true });
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const e of ents) {
|
|
255
|
+
if (e.isDirectory())
|
|
256
|
+
walk(path.join(abs, e.name), [...relParts, e.name]);
|
|
257
|
+
else if (e.isFile() && e.name.endsWith('.client.js')) {
|
|
258
|
+
const stem = e.name.replace(/\.client\.js$/, '');
|
|
259
|
+
if (stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound')
|
|
260
|
+
continue;
|
|
261
|
+
const chunkId = ['pages', ...relParts, stem].join('/');
|
|
262
|
+
out.push({ chunkId, segs: parseChunkSegments(chunkId) });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
walk(root, []);
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* @param {string} chunkId
|
|
271
|
+
*/
|
|
272
|
+
function parseChunkSegments(chunkId) {
|
|
273
|
+
const rel = chunkId.replace(/^pages\//, '');
|
|
274
|
+
const parts = rel.split('/').filter(Boolean);
|
|
275
|
+
/** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
|
|
276
|
+
const segs = [];
|
|
277
|
+
for (let i = 0; i < parts.length; i++) {
|
|
278
|
+
const p = parts[i];
|
|
279
|
+
if (p.startsWith('(') && p.endsWith(')') && p.length > 2)
|
|
280
|
+
continue;
|
|
281
|
+
if (p === 'index' && i === parts.length - 1)
|
|
282
|
+
continue;
|
|
283
|
+
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
|
|
284
|
+
const param = /^\[([^\]]+)\]$/.exec(p);
|
|
285
|
+
if (catchAll)
|
|
286
|
+
segs.push({ kind: 'catch', name: catchAll[1] });
|
|
287
|
+
else if (param)
|
|
288
|
+
segs.push({ kind: 'param', name: param[1] });
|
|
289
|
+
else
|
|
290
|
+
segs.push({ kind: 'static', value: p.toLowerCase() });
|
|
291
|
+
}
|
|
292
|
+
return segs;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* @param {ReturnType<typeof parseChunkSegments>} segs
|
|
296
|
+
*/
|
|
297
|
+
function patternFromSegs(segs) {
|
|
298
|
+
if (!segs.length)
|
|
299
|
+
return '/';
|
|
300
|
+
return `/${segs
|
|
301
|
+
.map((s) => {
|
|
302
|
+
if (s.kind === 'static')
|
|
303
|
+
return s.value;
|
|
304
|
+
if (s.kind === 'param')
|
|
305
|
+
return `[${s.name}]`;
|
|
306
|
+
return `[...${s.name}]`;
|
|
307
|
+
})
|
|
308
|
+
.join('/')}`;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* @param {string} pathname
|
|
312
|
+
*/
|
|
313
|
+
function htmlPathForRoute(pathname) {
|
|
314
|
+
const p = pathname === '/' ? '' : pathname.replace(/^\//, '').replace(/\/+$/, '');
|
|
315
|
+
if (!p)
|
|
316
|
+
return 'index.html';
|
|
317
|
+
return path.join(...p.split('/'), 'index.html');
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* @param {string} distDir
|
|
321
|
+
* @param {string} chunkId
|
|
322
|
+
*/
|
|
323
|
+
async function loadCtor(distDir, chunkId) {
|
|
324
|
+
const href = pathToFileURL(path.join(distDir, `${chunkId}.client.js`)).href;
|
|
325
|
+
const mod = await import(`${href}?t=${Date.now()}`);
|
|
326
|
+
return mod.default;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* @param {string} distDir
|
|
330
|
+
* @param {string} pageChunkId
|
|
331
|
+
*/
|
|
332
|
+
function resolveLayoutChain(distDir, pageChunkId) {
|
|
333
|
+
const rel = pageChunkId.replace(/^pages\//, '');
|
|
334
|
+
const parts = rel.split('/').filter(Boolean);
|
|
335
|
+
parts.pop();
|
|
336
|
+
/** @type {string[]} */
|
|
337
|
+
const chain = [];
|
|
338
|
+
for (let i = parts.length; i >= 0; i--) {
|
|
339
|
+
const dirParts = parts.slice(0, i);
|
|
340
|
+
const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
|
|
341
|
+
if (fs.existsSync(path.join(distDir, `${layoutChunk}.client.js`)))
|
|
342
|
+
chain.unshift(layoutChunk);
|
|
343
|
+
}
|
|
344
|
+
return chain;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* @param {string} distDir
|
|
348
|
+
* @param {string} chunkId
|
|
349
|
+
*/
|
|
350
|
+
function guessRouteId(distDir, chunkId) {
|
|
351
|
+
try {
|
|
352
|
+
const js = fs.readFileSync(path.join(distDir, `${chunkId}.client.js`), 'utf8');
|
|
353
|
+
const m = /export default class (\w+)/.exec(js);
|
|
354
|
+
if (m)
|
|
355
|
+
return m[1];
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
/* ignore */
|
|
359
|
+
}
|
|
360
|
+
return chunkId.split('/').pop() || chunkId;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* @param {any} Page
|
|
364
|
+
* @param {{ params: Record<string, string>, props: Record<string, unknown>, pathname: string, origin: string }} ctx
|
|
365
|
+
*/
|
|
366
|
+
async function resolvePageMeta(Page, ctx) {
|
|
367
|
+
let raw = {};
|
|
368
|
+
if (typeof Page.meta === 'function') {
|
|
369
|
+
raw = (await Page.meta(ctx)) || {};
|
|
370
|
+
}
|
|
371
|
+
else if (Page.meta && typeof Page.meta === 'object') {
|
|
372
|
+
raw = Page.meta;
|
|
373
|
+
}
|
|
374
|
+
const title = String(raw.title || `${guessTitle(ctx.pathname)} · VMZ`);
|
|
375
|
+
const description = String(raw.description || `VMZ page ${ctx.pathname}`);
|
|
376
|
+
const canonical = String(raw.canonical || `${ctx.origin}${ctx.pathname === '/' ? '/' : ctx.pathname}`);
|
|
377
|
+
const robots = String(raw.robots || 'index,follow');
|
|
378
|
+
const lang = String(raw.lang || 'en');
|
|
379
|
+
return { title, description, canonical, robots, lang };
|
|
380
|
+
}
|
|
381
|
+
function guessTitle(pathname) {
|
|
382
|
+
if (pathname === '/')
|
|
383
|
+
return 'Home';
|
|
384
|
+
return pathname
|
|
385
|
+
.split('/')
|
|
386
|
+
.filter(Boolean)
|
|
387
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
388
|
+
.join(' / ');
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* @param {string} distDir
|
|
392
|
+
*/
|
|
393
|
+
function readCssEntry(distDir) {
|
|
394
|
+
try {
|
|
395
|
+
const dep = JSON.parse(fs.readFileSync(path.join(distDir, 'vmz-deployment.json'), 'utf8'));
|
|
396
|
+
return dep.cssEntry || null;
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* @param {{
|
|
404
|
+
* bodyHtml: string,
|
|
405
|
+
* chunkId: string,
|
|
406
|
+
* layoutChain: string[],
|
|
407
|
+
* props: Record<string, unknown>,
|
|
408
|
+
* meta: { title: string, description: string, canonical: string, robots: string, lang: string },
|
|
409
|
+
* cssEntry: string | null,
|
|
410
|
+
* isErrorDocument?: boolean,
|
|
411
|
+
* }} input
|
|
412
|
+
*/
|
|
413
|
+
function wrapDocument(input) {
|
|
414
|
+
const propsJson = JSON.stringify(input.props ?? {});
|
|
415
|
+
const layoutAttr = input.layoutChain.length ? ` data-vmz-layout="${escapeAttr(input.layoutChain.join(','))}"` : '';
|
|
416
|
+
const pageAttr = input.chunkId ? ` data-vmz-page="${escapeAttr(input.chunkId)}"` : '';
|
|
417
|
+
const cssLink = input.cssEntry ? ` <link rel="stylesheet" href="/${String(input.cssEntry).replace(/^\/+/, '')}" />\n` : '';
|
|
418
|
+
const entry = input.isErrorDocument ? '' : ` <script type="module" src="/entry-client.js"></script>\n`;
|
|
419
|
+
return `<!DOCTYPE html>
|
|
420
|
+
<html lang="${escapeAttr(input.meta.lang)}">
|
|
421
|
+
<head>
|
|
422
|
+
<meta charset="utf-8" />
|
|
423
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
424
|
+
<title>${escapeHtml(input.meta.title)}</title>
|
|
425
|
+
<meta name="description" content="${escapeAttr(input.meta.description)}" />
|
|
426
|
+
<meta name="robots" content="${escapeAttr(input.meta.robots)}" />
|
|
427
|
+
<link rel="canonical" href="${escapeAttr(input.meta.canonical)}" />
|
|
428
|
+
<meta property="og:title" content="${escapeAttr(input.meta.title)}" />
|
|
429
|
+
<meta property="og:description" content="${escapeAttr(input.meta.description)}" />
|
|
430
|
+
<meta property="og:url" content="${escapeAttr(input.meta.canonical)}" />
|
|
431
|
+
${cssLink}</head>
|
|
432
|
+
<body>
|
|
433
|
+
<div id="app"${pageAttr}${layoutAttr} data-vmz-props="${escapeAttr(propsJson)}">${input.bodyHtml}</div>
|
|
434
|
+
${entry}</body>
|
|
435
|
+
</html>
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* @param {string} origin
|
|
440
|
+
* @param {Array<{ canonical: string, robots: string }>} generations
|
|
441
|
+
*/
|
|
442
|
+
function buildSitemap(origin, generations) {
|
|
443
|
+
const urls = generations
|
|
444
|
+
.filter((g) => !String(g.robots).includes('noindex'))
|
|
445
|
+
.map((g) => ` <url>
|
|
446
|
+
<loc>${escapeXml(g.canonical)}</loc>
|
|
447
|
+
</url>`)
|
|
448
|
+
.join('\n');
|
|
449
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
450
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
451
|
+
${urls}
|
|
452
|
+
</urlset>
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
function escapeHtml(s) {
|
|
456
|
+
return String(s).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
457
|
+
}
|
|
458
|
+
function escapeAttr(s) {
|
|
459
|
+
return escapeHtml(s).replaceAll('"', '"');
|
|
460
|
+
}
|
|
461
|
+
function escapeXml(s) {
|
|
462
|
+
return escapeAttr(s).replaceAll("'", ''');
|
|
463
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/vmz",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "VMZ Node toolchain — N-API workspace session + CLI",
|
|
5
|
+
"description": "VMZ Node toolchain — N-API workspace session + CLI (publish name @vmz/vmz)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"vmz": "./bin/vmz.js"
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@vmz/core": "0.0.
|
|
52
|
-
"@vmz/plugin": "0.0.
|
|
53
|
-
"@vmz/protocol": "0.0.
|
|
51
|
+
"@vmz/core": "0.0.4",
|
|
52
|
+
"@vmz/plugin": "0.0.4",
|
|
53
|
+
"@vmz/protocol": "0.0.4",
|
|
54
54
|
"jiti": "^2.6.1",
|
|
55
55
|
"json5": "^2.2.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@vmz/plugin-markdown-it": "0.0.
|
|
59
|
-
"@vmz/test": "0.0.
|
|
58
|
+
"@vmz/plugin-markdown-it": "0.0.4",
|
|
59
|
+
"@vmz/test": "0.0.4",
|
|
60
60
|
"typescript": "^5.8.3"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
@@ -90,12 +90,12 @@
|
|
|
90
90
|
"cli"
|
|
91
91
|
],
|
|
92
92
|
"optionalDependencies": {
|
|
93
|
-
"@vmz/vmz-win32-x64": "0.0.
|
|
94
|
-
"@vmz/vmz-win32-arm64": "0.0.
|
|
95
|
-
"@vmz/vmz-darwin-x64": "0.0.
|
|
96
|
-
"@vmz/vmz-darwin-arm64": "0.0.
|
|
97
|
-
"@vmz/vmz-linux-x64": "0.0.
|
|
98
|
-
"@vmz/vmz-linux-arm64": "0.0.
|
|
93
|
+
"@vmz/vmz-win32-x64": "0.0.4",
|
|
94
|
+
"@vmz/vmz-win32-arm64": "0.0.4",
|
|
95
|
+
"@vmz/vmz-darwin-x64": "0.0.4",
|
|
96
|
+
"@vmz/vmz-darwin-arm64": "0.0.4",
|
|
97
|
+
"@vmz/vmz-linux-x64": "0.0.4",
|
|
98
|
+
"@vmz/vmz-linux-arm64": "0.0.4"
|
|
99
99
|
},
|
|
100
100
|
"publishConfig": {
|
|
101
101
|
"access": "public"
|