mnfst-render 0.5.25 → 0.5.26
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/manifest.render.mjs +45 -140
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -214,102 +214,18 @@ function parseArgs() {
|
|
|
214
214
|
function loadConfig(rootDir) {
|
|
215
215
|
const manifestPath = join(rootDir, 'manifest.json');
|
|
216
216
|
if (!existsSync(manifestPath)) {
|
|
217
|
-
return {
|
|
217
|
+
return { prerender: {} };
|
|
218
218
|
}
|
|
219
219
|
const raw = readFileSync(manifestPath, 'utf8');
|
|
220
220
|
let manifest;
|
|
221
221
|
try {
|
|
222
222
|
manifest = JSON.parse(raw);
|
|
223
223
|
} catch {
|
|
224
|
-
return {
|
|
224
|
+
return { prerender: {} };
|
|
225
225
|
}
|
|
226
226
|
return manifest;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// Move credential-shaped values out of manifest.json into .env. Anything
|
|
230
|
-
// inlined into manifest.json ships verbatim into dist/ (the file is copied
|
|
231
|
-
// to the output unchanged), so a literal devKey there is a production leak.
|
|
232
|
-
// Rewriting source AND the in-memory object covers both directions: future
|
|
233
|
-
// renders see the placeholder, and the dist copy this run produces does too.
|
|
234
|
-
// Idempotent — values already shaped like ${VAR} are skipped.
|
|
235
|
-
function relocateSecretsToEnv(rootDir, manifest) {
|
|
236
|
-
const moves = [];
|
|
237
|
-
|
|
238
|
-
function maybeMove(obj, key, envVar, displayPath) {
|
|
239
|
-
if (!obj || typeof obj !== 'object') return;
|
|
240
|
-
const v = obj[key];
|
|
241
|
-
if (typeof v !== 'string' || !v) return;
|
|
242
|
-
if (/^\$\{[^}]+\}$/.test(v)) return; // already a placeholder
|
|
243
|
-
moves.push({ obj, key, value: v, envVar, displayPath });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
maybeMove(manifest.appwrite, 'devKey', 'APPWRITE_DEV_KEY', 'appwrite.devKey');
|
|
247
|
-
if (manifest.data && typeof manifest.data === 'object') {
|
|
248
|
-
for (const [srcName, src] of Object.entries(manifest.data)) {
|
|
249
|
-
maybeMove(src, 'appwriteDevKey', 'APPWRITE_DEV_KEY', `data.${srcName}.appwriteDevKey`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (moves.length === 0) return manifest;
|
|
254
|
-
|
|
255
|
-
for (const m of moves) {
|
|
256
|
-
m.obj[m.key] = `\${${m.envVar}}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const manifestPath = join(rootDir, 'manifest.json');
|
|
260
|
-
try {
|
|
261
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 4) + '\n');
|
|
262
|
-
} catch (err) {
|
|
263
|
-
console.error(`mnfst-render: failed to rewrite ${manifestPath}: ${err.message}`);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const envPath = join(rootDir, '.env');
|
|
268
|
-
let envText = '';
|
|
269
|
-
try {
|
|
270
|
-
if (existsSync(envPath)) envText = readFileSync(envPath, 'utf8');
|
|
271
|
-
} catch {}
|
|
272
|
-
if (envText && !envText.endsWith('\n')) envText += '\n';
|
|
273
|
-
|
|
274
|
-
const existingVars = new Set();
|
|
275
|
-
for (const line of envText.split(/\r?\n/)) {
|
|
276
|
-
const m = line.match(/^([A-Z_][A-Z0-9_]*)\s*=/);
|
|
277
|
-
if (m) existingVars.add(m[1]);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const additions = [];
|
|
281
|
-
for (const m of moves) {
|
|
282
|
-
if (!existingVars.has(m.envVar)) {
|
|
283
|
-
additions.push(`${m.envVar}=${m.value}`);
|
|
284
|
-
existingVars.add(m.envVar);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (additions.length) {
|
|
289
|
-
try {
|
|
290
|
-
writeFileSync(envPath, envText + additions.join('\n') + '\n');
|
|
291
|
-
} catch (err) {
|
|
292
|
-
console.error(`mnfst-render: failed to write ${envPath}: ${err.message}`);
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
console.warn('');
|
|
298
|
-
console.warn('mnfst-render: relocated credentials from manifest.json to .env');
|
|
299
|
-
for (const m of moves) {
|
|
300
|
-
console.warn(` • ${m.displayPath} → \${${m.envVar}}`);
|
|
301
|
-
}
|
|
302
|
-
if (additions.length) {
|
|
303
|
-
console.warn(` Appended ${additions.length} var(s) to .env (verify .env is in .gitignore).`);
|
|
304
|
-
} else {
|
|
305
|
-
console.warn(' .env already had matching vars; manifest.json placeholders now point at them.');
|
|
306
|
-
}
|
|
307
|
-
console.warn(' Browser-side dev needs window.env populated separately (e.g. env.local.js).');
|
|
308
|
-
console.warn('');
|
|
309
|
-
|
|
310
|
-
return manifest;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
229
|
function normalizeLocaleRouteExclude(val) {
|
|
314
230
|
if (val == null) return [];
|
|
315
231
|
if (Array.isArray(val)) return val.map((s) => String(s).trim()).filter(Boolean);
|
|
@@ -321,44 +237,44 @@ function resolveConfig() {
|
|
|
321
237
|
const cli = parseArgs();
|
|
322
238
|
const cwd = process.cwd();
|
|
323
239
|
const root = resolve(cwd, cli.root ?? '.');
|
|
324
|
-
const manifest =
|
|
325
|
-
const
|
|
240
|
+
const manifest = loadConfig(root);
|
|
241
|
+
const pre = manifest.prerender ?? {};
|
|
326
242
|
|
|
327
|
-
const localUrl = (cli.localUrl ?? cli.baseUrl ?? process.env.PRERENDER_BASE ??
|
|
243
|
+
const localUrl = (cli.localUrl ?? cli.baseUrl ?? process.env.PRERENDER_BASE ?? pre.localUrl ?? pre.baseUrl)?.replace(/\/$/, '');
|
|
328
244
|
const serve = cli.localUrl ? false : (cli.serve !== undefined ? !!cli.serve : true);
|
|
329
245
|
if (!serve && !localUrl) {
|
|
330
|
-
console.error('prerender: localUrl is required when not using built-in server. Set manifest.
|
|
246
|
+
console.error('prerender: localUrl is required when not using built-in server. Set manifest.prerender.localUrl or use --local.');
|
|
331
247
|
process.exit(1);
|
|
332
248
|
}
|
|
333
|
-
const liveUrl = (cli.liveUrl ?? process.env.PRERENDER_LIVE ?? manifest.live_url ?? manifest.liveUrl ??
|
|
249
|
+
const liveUrl = (cli.liveUrl ?? process.env.PRERENDER_LIVE ?? manifest.live_url ?? manifest.liveUrl ?? pre.live_url ?? pre.liveUrl ?? localUrl ?? '')?.replace(/\/$/, '');
|
|
334
250
|
|
|
335
251
|
return {
|
|
336
252
|
localUrl: localUrl ?? '',
|
|
337
253
|
liveUrl,
|
|
338
254
|
serve,
|
|
339
|
-
output: resolve(root, cli.output ??
|
|
255
|
+
output: resolve(root, cli.output ?? pre.output ?? 'website'),
|
|
340
256
|
root,
|
|
341
|
-
routerBase:
|
|
257
|
+
routerBase: pre.routerBase ?? null,
|
|
342
258
|
/** Logical path prefixes (after locale) that skip sticky locale prefix; see manifest:locale-route-exclude */
|
|
343
259
|
localeRouteExclude: normalizeLocaleRouteExclude(
|
|
344
|
-
|
|
260
|
+
pre.localeRouteExclude ?? pre.localeStickyExclude
|
|
345
261
|
),
|
|
346
|
-
locales:
|
|
347
|
-
redirects: Array.isArray(
|
|
348
|
-
wait: cli.wait ??
|
|
262
|
+
locales: pre.locales,
|
|
263
|
+
redirects: Array.isArray(pre.redirects) ? pre.redirects : [],
|
|
264
|
+
wait: cli.wait ?? pre.wait ?? null,
|
|
349
265
|
waitAfterIdle: 0,
|
|
350
266
|
// Default concurrency: 2. Chromium per-page memory overhead is large and
|
|
351
267
|
// our hydration source-attribute map adds more per page. On big sites
|
|
352
268
|
// (>100 routes) higher concurrency crashes the browser with OOM/target
|
|
353
269
|
// closed errors. Users can override for small projects with --concurrency.
|
|
354
|
-
concurrency: Math.max(1, cli.concurrency ??
|
|
355
|
-
retries: Math.max(0, cli.retries ??
|
|
270
|
+
concurrency: Math.max(1, cli.concurrency ?? pre.concurrency ?? 2),
|
|
271
|
+
retries: Math.max(0, cli.retries ?? pre.retries ?? 2),
|
|
356
272
|
localeSubstitution: true,
|
|
357
273
|
localeSubstitutionExclude: [],
|
|
358
274
|
/** Explicit locale-neutral paths to render in addition to those discovered automatically.
|
|
359
275
|
* Each entry is expanded to all locale variants (e.g. "legal/privacy" → "cs/legal/privacy", ...) */
|
|
360
|
-
paths: Array.isArray(
|
|
361
|
-
?
|
|
276
|
+
paths: Array.isArray(pre.paths)
|
|
277
|
+
? pre.paths.map((p) => String(p).replace(/^\/+|\/+$/g, '')).filter(Boolean)
|
|
362
278
|
: [],
|
|
363
279
|
dryRun: !!cli.dryRun,
|
|
364
280
|
debugPrerender: !!cli.debugPrerender,
|
|
@@ -367,12 +283,12 @@ function resolveConfig() {
|
|
|
367
283
|
// fall back to the timeout. 10s gives slow data plugin pipelines a
|
|
368
284
|
// chance while bounding worst-case per-path overhead.
|
|
369
285
|
pipelineTimeout: 10000,
|
|
370
|
-
// SEO / AEO meta injection — see metaInjection() and the
|
|
286
|
+
// SEO / AEO meta injection — see metaInjection() and the prerender.meta
|
|
371
287
|
// section of manifest.json. Layered precedence (highest first):
|
|
372
288
|
// 1. <template data-head> per-route (already in DOM at snapshot time)
|
|
373
289
|
// 2. <head> in index.html (already in DOM at snapshot time)
|
|
374
|
-
// 3.
|
|
375
|
-
// 4.
|
|
290
|
+
// 3. prerender.meta.* expressions (Alpine-evaluated per route)
|
|
291
|
+
// 4. prerender.meta.fallback.* (static strings if expression empty)
|
|
376
292
|
// 5. PWA-style manifest.json fields (name, description, author, icons)
|
|
377
293
|
// 6. Smart defaults derived from the rendered DOM (h1, first p, etc.)
|
|
378
294
|
//
|
|
@@ -383,10 +299,10 @@ function resolveConfig() {
|
|
|
383
299
|
siteDescription: manifest.description || null,
|
|
384
300
|
siteAuthor: manifest.author || null,
|
|
385
301
|
icons: Array.isArray(manifest.icons) ? manifest.icons : [],
|
|
386
|
-
meta:
|
|
387
|
-
structuredData:
|
|
388
|
-
imageSnapshots:
|
|
389
|
-
defaults:
|
|
302
|
+
meta: pre.meta || null,
|
|
303
|
+
structuredData: pre.structuredData || null,
|
|
304
|
+
imageSnapshots: pre.meta?.imageSnapshots !== false, // default true
|
|
305
|
+
defaults: pre.meta?.defaults !== false, // default true
|
|
390
306
|
},
|
|
391
307
|
};
|
|
392
308
|
}
|
|
@@ -853,18 +769,18 @@ function debakeThemeClass(html) {
|
|
|
853
769
|
return out;
|
|
854
770
|
}
|
|
855
771
|
|
|
856
|
-
/** Manifest utilities plugin: <style id="
|
|
772
|
+
/** Manifest utilities plugin: <style id="manifest-styles"> and <style id="manifest-styles-critical"> */
|
|
857
773
|
function extractUtilityStyleBlocks(html) {
|
|
858
774
|
const blocks = [];
|
|
859
775
|
let out = html.replace(
|
|
860
|
-
/<style[^>]*\bid=["']
|
|
776
|
+
/<style[^>]*\bid=["']manifest-styles-critical["'][^>]*>([\s\S]*?)<\/style>/gi,
|
|
861
777
|
(_, css) => {
|
|
862
778
|
const t = (css || '').trim();
|
|
863
779
|
if (t) blocks.push({ kind: 'critical', css: t });
|
|
864
780
|
return '';
|
|
865
781
|
}
|
|
866
782
|
);
|
|
867
|
-
out = out.replace(/<style[^>]*\bid=["']
|
|
783
|
+
out = out.replace(/<style[^>]*\bid=["']manifest-styles["'][^>]*>([\s\S]*?)<\/style>/gi, (_, css) => {
|
|
868
784
|
const t = (css || '').trim();
|
|
869
785
|
if (t) blocks.push({ kind: 'main', css: t });
|
|
870
786
|
return '';
|
|
@@ -926,9 +842,9 @@ function promptContinueWithRuntimeTailwind(rootDir) {
|
|
|
926
842
|
/**
|
|
927
843
|
* Build a static Tailwind stylesheet via @tailwindcss/cli (v4+), scanning project sources.
|
|
928
844
|
* Only runs when the project uses data-tailwind on the manifest script tag (auto-detected).
|
|
929
|
-
* Set manifest.
|
|
845
|
+
* Set manifest.prerender.tailwindInput to a custom CSS entry file if needed.
|
|
930
846
|
*/
|
|
931
|
-
function runTailwindCliForPrerender(rootDir, outputDir,
|
|
847
|
+
function runTailwindCliForPrerender(rootDir, outputDir, pre) {
|
|
932
848
|
if (!indexHtmlUsesTailwind(rootDir)) return false;
|
|
933
849
|
|
|
934
850
|
const outCss = join(outputDir, 'prerender.tailwind.css');
|
|
@@ -944,7 +860,7 @@ function runTailwindCliForPrerender(rootDir, outputDir, ren) {
|
|
|
944
860
|
}
|
|
945
861
|
let inputPath = null;
|
|
946
862
|
let createdTempInput = false;
|
|
947
|
-
const userInput =
|
|
863
|
+
const userInput = pre?.tailwindInput;
|
|
948
864
|
if (typeof userInput === 'string' && userInput.trim()) {
|
|
949
865
|
inputPath = resolve(rootDir, userInput.trim());
|
|
950
866
|
}
|
|
@@ -975,15 +891,10 @@ function runTailwindCliForPrerender(rootDir, outputDir, ren) {
|
|
|
975
891
|
}
|
|
976
892
|
|
|
977
893
|
process.stdout.write('prerender: compiling Tailwind CSS (this may take a minute)...\n');
|
|
978
|
-
|
|
979
|
-
// `shell: true`. With `shell: true`, every argument (including the user-
|
|
980
|
-
// controlled `tailwindInput` path from manifest.json) gets parsed by cmd.exe
|
|
981
|
-
// — so a value like `styles.css & evilcmd` would run `evilcmd`. Calling
|
|
982
|
-
// npx.cmd directly keeps args as a literal argv with no shell interpretation.
|
|
983
|
-
const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
984
|
-
const r = spawnSync(command, args, {
|
|
894
|
+
const r = spawnSync('npx', args, {
|
|
985
895
|
cwd: rootDir,
|
|
986
896
|
encoding: 'utf8',
|
|
897
|
+
shell: process.platform === 'win32',
|
|
987
898
|
});
|
|
988
899
|
if (createdTempInput) {
|
|
989
900
|
try {
|
|
@@ -993,7 +904,7 @@ function runTailwindCliForPrerender(rootDir, outputDir, ren) {
|
|
|
993
904
|
}
|
|
994
905
|
}
|
|
995
906
|
if (r.status !== 0) {
|
|
996
|
-
console.error('prerender: Tailwind CLI failed; install with `npm i -D tailwindcss @tailwindcss/cli` or check tailwindInput in manifest.
|
|
907
|
+
console.error('prerender: Tailwind CLI failed; install with `npm i -D tailwindcss @tailwindcss/cli` or check tailwindInput in manifest.prerender.');
|
|
997
908
|
if (r.stderr) console.error(r.stderr);
|
|
998
909
|
if (r.stdout) console.error(r.stdout);
|
|
999
910
|
return false;
|
|
@@ -2847,7 +2758,7 @@ async function runPrerender(config) {
|
|
|
2847
2758
|
|
|
2848
2759
|
const defaultLocale = locales[0] ?? null;
|
|
2849
2760
|
const routeSegments = discoverRoutes(manifest, config.root);
|
|
2850
|
-
// Merge any explicitly configured paths (manifest.
|
|
2761
|
+
// Merge any explicitly configured paths (manifest.prerender.paths) into the discovered segments.
|
|
2851
2762
|
// These are treated as locale-neutral and get full locale-expansion like all other discovered paths.
|
|
2852
2763
|
if (config.paths && config.paths.length > 0) {
|
|
2853
2764
|
const segSet = new Set(routeSegments);
|
|
@@ -2894,7 +2805,7 @@ async function runPrerender(config) {
|
|
|
2894
2805
|
const outputResolved = resolve(config.output);
|
|
2895
2806
|
const rootResolved = resolve(config.root);
|
|
2896
2807
|
// Router base = URL pathname to the app root. When dist is deployed as site root (e.g. Appwrite), use "".
|
|
2897
|
-
// Set manifest.
|
|
2808
|
+
// Set manifest.prerender.routerBase only when the app is served from a subpath (e.g. /app).
|
|
2898
2809
|
let routerBasePath = null;
|
|
2899
2810
|
if (config.routerBase != null && String(config.routerBase).trim() !== '') {
|
|
2900
2811
|
const trimmed = String(config.routerBase).replace(/^\/+|\/+$/g, '').trim();
|
|
@@ -2909,9 +2820,9 @@ async function runPrerender(config) {
|
|
|
2909
2820
|
mkdirSync(outputResolved, { recursive: true });
|
|
2910
2821
|
copyProjectIntoDist(rootResolved, outputResolved);
|
|
2911
2822
|
|
|
2912
|
-
const
|
|
2913
|
-
const bundleUtilities =
|
|
2914
|
-
const tailwindBuilt = runTailwindCliForPrerender(rootResolved, outputResolved,
|
|
2823
|
+
const pre = manifest.prerender ?? {};
|
|
2824
|
+
const bundleUtilities = pre.utilitiesBundle !== false;
|
|
2825
|
+
const tailwindBuilt = runTailwindCliForPrerender(rootResolved, outputResolved, pre);
|
|
2915
2826
|
const utilityBlocks = [];
|
|
2916
2827
|
|
|
2917
2828
|
// Launch a fresh browser instance. Chromium is known to accumulate memory
|
|
@@ -2943,18 +2854,12 @@ async function runPrerender(config) {
|
|
|
2943
2854
|
console.error(' npm i -D puppeteer-core @sparticuz/chromium');
|
|
2944
2855
|
process.exit(1);
|
|
2945
2856
|
}
|
|
2946
|
-
// Chrome's sandbox is the primary defense against renderer-process
|
|
2947
|
-
// exploits — disabling it means any RCE in a rendered page runs as the
|
|
2948
|
-
// developer's UID with full filesystem access. We render arbitrary CDN
|
|
2949
|
-
// scripts and third-party iframes, so the threat is real. Opt-in only
|
|
2950
|
-
// for CI environments where the sandbox legitimately can't initialize:
|
|
2951
|
-
// set MNFST_RENDER_NO_SANDBOX=1 to add the flags back.
|
|
2952
|
-
const extraArgs = process.env.MNFST_RENDER_NO_SANDBOX === '1'
|
|
2953
|
-
? ['--no-sandbox', '--disable-setuid-sandbox']
|
|
2954
|
-
: [];
|
|
2955
2857
|
return await puppeteer.default.launch({
|
|
2956
2858
|
headless: true,
|
|
2957
|
-
args:
|
|
2859
|
+
args: [
|
|
2860
|
+
'--no-sandbox',
|
|
2861
|
+
'--disable-setuid-sandbox',
|
|
2862
|
+
],
|
|
2958
2863
|
});
|
|
2959
2864
|
}
|
|
2960
2865
|
}
|
|
@@ -2965,12 +2870,12 @@ async function runPrerender(config) {
|
|
|
2965
2870
|
// substantial, and we also now maintain a per-page source-attribute Map for
|
|
2966
2871
|
// the hydration contract. On large sites (>100 routes) higher concurrency
|
|
2967
2872
|
// spikes memory and crashes the browser. Users can still override via
|
|
2968
|
-
// --concurrency or manifest.
|
|
2873
|
+
// --concurrency or manifest.prerender.concurrency.
|
|
2969
2874
|
const concurrency = config.concurrency;
|
|
2970
2875
|
const maxRetries = config.retries ?? 2;
|
|
2971
2876
|
// Recycle the browser every N processed pages to bound resource growth.
|
|
2972
|
-
// Configurable via manifest.
|
|
2973
|
-
const browserRecycleEvery = Math.max(0,
|
|
2877
|
+
// Configurable via manifest.prerender.browserRecycleEvery.
|
|
2878
|
+
const browserRecycleEvery = Math.max(0, pre.browserRecycleEvery ?? 40);
|
|
2974
2879
|
let pagesSinceRecycle = 0;
|
|
2975
2880
|
const recycleLock = { busy: false };
|
|
2976
2881
|
// Workers block on this promise before touching `browser`. While a recycle
|