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.
Files changed (2) hide show
  1. package/manifest.render.mjs +45 -140
  2. package/package.json +1 -1
@@ -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 { render: {} };
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 { render: {} };
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 = relocateSecretsToEnv(root, loadConfig(root));
325
- const ren = manifest.render ?? {};
240
+ const manifest = loadConfig(root);
241
+ const pre = manifest.prerender ?? {};
326
242
 
327
- const localUrl = (cli.localUrl ?? cli.baseUrl ?? process.env.PRERENDER_BASE ?? ren.localUrl ?? ren.baseUrl)?.replace(/\/$/, '');
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.render.localUrl or use --local.');
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 ?? ren.live_url ?? ren.liveUrl ?? localUrl ?? '')?.replace(/\/$/, '');
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 ?? ren.output ?? 'website'),
255
+ output: resolve(root, cli.output ?? pre.output ?? 'website'),
340
256
  root,
341
- routerBase: ren.routerBase ?? null,
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
- ren.localeRouteExclude ?? ren.localeStickyExclude
260
+ pre.localeRouteExclude ?? pre.localeStickyExclude
345
261
  ),
346
- locales: ren.locales,
347
- redirects: Array.isArray(ren.redirects) ? ren.redirects : [],
348
- wait: cli.wait ?? ren.wait ?? null,
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 ?? ren.concurrency ?? 2),
355
- retries: Math.max(0, cli.retries ?? ren.retries ?? 2),
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(ren.paths)
361
- ? ren.paths.map((p) => String(p).replace(/^\/+|\/+$/g, '')).filter(Boolean)
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 render.meta
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. render.meta.* expressions (Alpine-evaluated per route)
375
- // 4. render.meta.fallback.* (static strings if expression empty)
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: ren.meta || null,
387
- structuredData: ren.structuredData || null,
388
- imageSnapshots: ren.meta?.imageSnapshots !== false, // default true
389
- defaults: ren.meta?.defaults !== false, // default true
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="utility-styles"> and <style id="utility-styles-critical"> */
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=["']utility-styles-critical["'][^>]*>([\s\S]*?)<\/style>/gi,
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=["']utility-styles["'][^>]*>([\s\S]*?)<\/style>/gi, (_, css) => {
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.render.tailwindInput to a custom CSS entry file if needed.
845
+ * Set manifest.prerender.tailwindInput to a custom CSS entry file if needed.
930
846
  */
931
- function runTailwindCliForPrerender(rootDir, outputDir, ren) {
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 = ren?.tailwindInput;
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
- // On Windows, invoke the .cmd shim directly instead of routing through
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.render.');
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.render.paths) into the discovered segments.
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.render.routerBase only when the app is served from a subpath (e.g. /app).
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 ren = manifest.render ?? {};
2913
- const bundleUtilities = ren.utilitiesBundle !== false;
2914
- const tailwindBuilt = runTailwindCliForPrerender(rootResolved, outputResolved, ren);
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: extraArgs,
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.render.concurrency.
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.render.browserRecycleEvery.
2973
- const browserRecycleEvery = Math.max(0, ren.browserRecycleEvery ?? 40);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.25",
3
+ "version": "0.5.26",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {