mnfst-render 0.1.5 → 0.1.7

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 +318 -21
  2. package/package.json +2 -2
@@ -2,8 +2,9 @@
2
2
 
3
3
  /* Manifest Render */
4
4
 
5
- import { readFileSync, mkdirSync, writeFileSync, existsSync, rmSync, statSync, readdirSync, cpSync } from 'node:fs';
6
- import { join, resolve, dirname, relative, basename } from 'node:path';
5
+ import { readFileSync, mkdirSync, writeFileSync, existsSync, rmSync, statSync, readdirSync, cpSync, unlinkSync } from 'node:fs';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { join, resolve, dirname, relative, basename, sep } from 'node:path';
7
8
  import { createServer } from 'node:http';
8
9
  import { createRequire } from 'node:module';
9
10
  import { fileURLToPath } from 'node:url';
@@ -144,20 +145,50 @@ function extractXRouteConditions(html) {
144
145
  return conditions;
145
146
  }
146
147
 
148
+ function normalizeRouteCondition(cond) {
149
+ const raw = String(cond || '').trim();
150
+ if (!raw) return { kind: 'all', path: '' };
151
+ if (raw.startsWith('!')) {
152
+ const omitted = raw.slice(1).trim();
153
+ if (!omitted || omitted === '*') return { kind: 'not-found', path: '' }; // !*
154
+ return { kind: 'omit', path: omitted };
155
+ }
156
+ if (raw === '*') return { kind: 'all', path: '' };
157
+ const withoutExact = raw.startsWith('=') ? raw.slice(1) : raw;
158
+ const trimmed = withoutExact.replace(/^\/+|\/+$/g, '');
159
+ if (!trimmed) return { kind: 'root', path: '' };
160
+ if (trimmed.endsWith('/*')) {
161
+ const base = trimmed.slice(0, -2).replace(/^\/+|\/+$/g, '');
162
+ return base ? { kind: 'wildcard-prefix', path: base } : { kind: 'all', path: '' };
163
+ }
164
+ if (trimmed.includes('*')) return { kind: 'unsupported-pattern', path: trimmed };
165
+ return { kind: 'path', path: trimmed };
166
+ }
167
+
147
168
  function conditionsToPaths(conditions) {
148
169
  const paths = new Set();
149
170
  paths.add('/');
150
171
  for (const c of conditions) {
151
- if (c === '/' || c === '') continue;
152
- if (c.startsWith('/')) {
153
- paths.add(c === '/' ? '/' : c.replace(/^\//, '').replace(/\/$/, '') || '/');
154
- } else {
155
- paths.add('/' + c.replace(/\/$/, ''));
156
- }
172
+ const parsed = normalizeRouteCondition(c);
173
+ // Discovery rules aligned with router docs:
174
+ // - "*" and omitted routes do not define concrete paths.
175
+ // - "!*" is handled separately via explicit NOT_FOUND path.
176
+ // - "about/*" does not include "/about" by itself; concrete children come from data paths.
177
+ if (parsed.kind === 'path') paths.add('/' + parsed.path);
178
+ else if (parsed.kind === 'root') paths.add('/');
157
179
  }
158
180
  return paths;
159
181
  }
160
182
 
183
+ function getWildcardBasesFromConditions(conditions) {
184
+ const bases = new Set();
185
+ for (const c of conditions) {
186
+ const parsed = normalizeRouteCondition(c);
187
+ if (parsed.kind === 'wildcard-prefix' && parsed.path) bases.add(parsed.path);
188
+ }
189
+ return [...bases];
190
+ }
191
+
161
192
  // --- Discovery: data-driven paths (docs-style YAML group/items[].path) ------
162
193
 
163
194
  function parseYamlPaths(filePath) {
@@ -177,6 +208,13 @@ function parseYamlPaths(filePath) {
177
208
  const segment = pathMatch[1].trim();
178
209
  paths.push(`${currentGroup}/${segment}`);
179
210
  }
211
+ const genericPathMatch = line.match(/^\s*(?:-\s*)?(?:path|slug):\s*["']?([^"'\n#]+)["']?/);
212
+ if (genericPathMatch) {
213
+ const v = genericPathMatch[1].trim().replace(/^\/+|\/+$/g, '');
214
+ if (v && !v.includes('*') && !/\.[a-z0-9]+$/i.test(v)) {
215
+ paths.push(v);
216
+ }
217
+ }
180
218
  }
181
219
  return paths;
182
220
  }
@@ -260,20 +298,38 @@ function parseCsvPaths(filePath) {
260
298
  return paths;
261
299
  }
262
300
 
263
- function discoverDataPaths(manifest, rootDir) {
301
+ function discoverDataPaths(manifest, rootDir, wildcardBases = [], locales = []) {
264
302
  const paths = new Set();
265
303
  const data = manifest.data;
266
304
  if (!data || typeof data !== 'object') return paths;
305
+ const localeSet = new Set((locales || []).map((l) => String(l).toLowerCase()));
306
+
307
+ function shouldIncludeDataPath(rawPath) {
308
+ const p = String(rawPath || '').replace(/^\/+|\/+$/g, '');
309
+ if (!p || p.includes('#') || p.includes('?') || p.includes('*')) return false;
310
+ if (wildcardBases.length === 0) return true;
311
+ const segs = p.split('/');
312
+ const rest = segs.length > 1 && localeSet.has(segs[0].toLowerCase()) ? segs.slice(1).join('/') : p;
313
+ return wildcardBases.some((base) => rest.startsWith(base + '/'));
314
+ }
267
315
 
268
316
  function addFilePaths(value) {
269
317
  if (typeof value !== 'string' || !value.startsWith('/')) return;
270
318
  const filePath = join(rootDir, value.slice(1));
271
319
  if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
272
- parseYamlPaths(filePath).forEach((p) => paths.add('/' + p));
320
+ parseYamlPaths(filePath).forEach((p) => {
321
+ if (shouldIncludeDataPath(p)) paths.add('/' + p);
322
+ });
273
323
  } else if (filePath.endsWith('.json')) {
274
- parseJsonPaths(filePath).forEach((p) => paths.add(p.startsWith('/') ? p : '/' + p));
324
+ parseJsonPaths(filePath).forEach((p) => {
325
+ const normalized = p.startsWith('/') ? p.slice(1) : p;
326
+ if (shouldIncludeDataPath(normalized)) paths.add('/' + normalized);
327
+ });
275
328
  } else if (filePath.endsWith('.csv')) {
276
- parseCsvPaths(filePath).forEach((p) => paths.add(p.startsWith('/') ? p : '/' + p));
329
+ parseCsvPaths(filePath).forEach((p) => {
330
+ const normalized = p.startsWith('/') ? p.slice(1) : p;
331
+ if (shouldIncludeDataPath(normalized)) paths.add('/' + normalized);
332
+ });
277
333
  }
278
334
  }
279
335
 
@@ -293,11 +349,14 @@ function discoverDataPaths(manifest, rootDir) {
293
349
  function discoverRoutes(manifest, rootDir) {
294
350
  const pathSet = new Set();
295
351
  pathSet.add('/');
352
+ const allConditions = new Set();
353
+ const locales = discoverLocales(manifest, rootDir);
296
354
 
297
355
  const indexPath = join(rootDir, 'index.html');
298
356
  if (existsSync(indexPath)) {
299
357
  const indexHtml = readFileSync(indexPath, 'utf8');
300
358
  const conditions = extractXRouteConditions(indexHtml);
359
+ conditions.forEach((c) => allConditions.add(c));
301
360
  conditionsToPaths(conditions).forEach((p) => pathSet.add(p));
302
361
  }
303
362
 
@@ -309,14 +368,14 @@ function discoverRoutes(manifest, rootDir) {
309
368
  const compPath = join(rootDir, rel);
310
369
  if (existsSync(compPath)) {
311
370
  const html = readFileSync(compPath, 'utf8');
312
- extractXRouteConditions(html).forEach((c) => {
313
- if (c.startsWith('/')) pathSet.add(c === '/' ? '/' : c.replace(/^\//, '').replace(/\/$/, '') || '/');
314
- else if (c) pathSet.add('/' + c);
315
- });
371
+ const conditions = extractXRouteConditions(html);
372
+ conditions.forEach((c) => allConditions.add(c));
373
+ conditionsToPaths(conditions).forEach((p) => pathSet.add(p));
316
374
  }
317
375
  }
318
376
 
319
- discoverDataPaths(manifest, rootDir).forEach((p) => pathSet.add(p));
377
+ const wildcardBases = getWildcardBasesFromConditions(allConditions);
378
+ discoverDataPaths(manifest, rootDir, wildcardBases, locales).forEach((p) => pathSet.add(p));
320
379
 
321
380
  const arr = [...pathSet].map((p) => (p === '/' ? '' : p.replace(/^\//, '').replace(/\/$/, '') || ''));
322
381
  return arr.includes('') ? arr : ['', ...arr.filter(Boolean)];
@@ -329,6 +388,29 @@ function pathToFileSegments(pathname) {
329
388
  return normalized ? normalized.split('/') : [];
330
389
  }
331
390
 
391
+ function validatePrerenderedOutput(outputDir, pathList) {
392
+ const invalidPathTokens = pathList.filter((p) => /(^|\/)[*=]/.test(p) || p.includes('/*') || p.includes('*'));
393
+ if (invalidPathTokens.length > 0) {
394
+ throw new Error(`prerender validation failed: invalid discovered route token(s): ${invalidPathTokens.join(', ')}`);
395
+ }
396
+
397
+ const badFolders = [];
398
+ function walk(dir, rel = '') {
399
+ const entries = readdirSync(dir, { withFileTypes: true });
400
+ for (const ent of entries) {
401
+ if (!ent.isDirectory()) continue;
402
+ const seg = ent.name;
403
+ const nextRel = rel ? `${rel}/${seg}` : seg;
404
+ if (seg.includes('*') || seg.startsWith('=')) badFolders.push(nextRel);
405
+ walk(join(dir, seg), nextRel);
406
+ }
407
+ }
408
+ if (existsSync(outputDir)) walk(outputDir, '');
409
+ if (badFolders.length > 0) {
410
+ throw new Error(`prerender validation failed: invalid output folder(s): ${badFolders.join(', ')}`);
411
+ }
412
+ }
413
+
332
414
  // --- Strip dev-only injected content (e.g. browser-sync) so dist works under any server -
333
415
 
334
416
  function stripDevOnlyContent(html) {
@@ -350,6 +432,181 @@ function stripInjectedPluginScripts(html) {
350
432
  return out;
351
433
  }
352
434
 
435
+ function stripRuntimeTailwindArtifacts(html) {
436
+ let out = html.replace(/\sdata-tailwind(?:=(["']).*?\1)?/gi, '');
437
+ // Remove PlayCDN-injected runtime Tailwind stylesheet from snapshots.
438
+ out = out.replace(/<style>\s*\/\*!\s*tailwindcss[\s\S]*?<\/style>/gi, '');
439
+ return out;
440
+ }
441
+
442
+ /** Manifest utilities plugin: <style id="utility-styles"> and <style id="utility-styles-critical"> */
443
+ function extractUtilityStyleBlocks(html) {
444
+ const blocks = [];
445
+ let out = html.replace(
446
+ /<style[^>]*\bid=["']utility-styles-critical["'][^>]*>([\s\S]*?)<\/style>/gi,
447
+ (_, css) => {
448
+ const t = (css || '').trim();
449
+ if (t) blocks.push({ kind: 'critical', css: t });
450
+ return '';
451
+ }
452
+ );
453
+ out = out.replace(/<style[^>]*\bid=["']utility-styles["'][^>]*>([\s\S]*?)<\/style>/gi, (_, css) => {
454
+ const t = (css || '').trim();
455
+ if (t) blocks.push({ kind: 'main', css: t });
456
+ return '';
457
+ });
458
+ return { html: out, blocks };
459
+ }
460
+
461
+ function injectAfterHeadOpen(html, snippet) {
462
+ if (!snippet) return html;
463
+ const hrefMatch = snippet.match(/href=["']([^"']+)["']/);
464
+ if (hrefMatch && html.includes(hrefMatch[1])) return html;
465
+ return html.replace(/<head([^>]*)>/i, `<head$1>\n${snippet}\n`);
466
+ }
467
+
468
+ function indexHtmlUsesTailwind(rootDir) {
469
+ const indexPath = join(rootDir, 'index.html');
470
+ if (!existsSync(indexPath)) return false;
471
+ const html = readFileSync(indexPath, 'utf8');
472
+ return /\sdata-tailwind(?:=(["']).*?\1)?/i.test(html) && /<script[^>]*manifest\.min\.js/i.test(html);
473
+ }
474
+
475
+ /**
476
+ * Build a static Tailwind stylesheet via @tailwindcss/cli (v4+), scanning project sources.
477
+ * Only runs when the project opts in (data-tailwind on manifest script) or manifest.prerender.tailwind === true.
478
+ */
479
+ function runTailwindCliForPrerender(rootDir, outputDir, pre) {
480
+ const explicit = pre?.tailwind;
481
+ if (explicit === false) return false;
482
+ const usesTailwind = explicit === true || indexHtmlUsesTailwind(rootDir);
483
+ if (!usesTailwind) return false;
484
+
485
+ const outCss = join(outputDir, 'prerender.tailwind.css');
486
+ let inputPath = null;
487
+ let createdTempInput = false;
488
+ const userInput = pre?.tailwindInput;
489
+ if (typeof userInput === 'string' && userInput.trim()) {
490
+ inputPath = resolve(rootDir, userInput.trim());
491
+ }
492
+ if (!inputPath || !existsSync(inputPath)) {
493
+ inputPath = join(rootDir, '.mnfst-prerender-tailwind-input.css');
494
+ writeFileSync(inputPath, '@import "tailwindcss";\n', 'utf8');
495
+ createdTempInput = true;
496
+ }
497
+
498
+ const outputBasename = basename(outputDir);
499
+ const defaultContent = [
500
+ '**/*.html',
501
+ '**/*.{js,mjs,css}',
502
+ '**/*.json',
503
+ '!**/node_modules/**',
504
+ `!**/${outputBasename}/**`,
505
+ ];
506
+ const contentGlobs = Array.isArray(pre?.tailwindContent) && pre.tailwindContent.length > 0
507
+ ? pre.tailwindContent
508
+ : defaultContent;
509
+
510
+ const args = [
511
+ '--yes',
512
+ '@tailwindcss/cli@4',
513
+ '-i',
514
+ inputPath,
515
+ '-o',
516
+ outCss,
517
+ ];
518
+ for (const g of contentGlobs) {
519
+ args.push('--content', g);
520
+ }
521
+
522
+ process.stdout.write('prerender: compiling Tailwind CSS (this may take a minute)...\n');
523
+ const r = spawnSync('npx', args, {
524
+ cwd: rootDir,
525
+ encoding: 'utf8',
526
+ shell: process.platform === 'win32',
527
+ });
528
+ if (createdTempInput) {
529
+ try {
530
+ unlinkSync(inputPath);
531
+ } catch {
532
+ // ignore
533
+ }
534
+ }
535
+ if (r.status !== 0) {
536
+ console.error('prerender: Tailwind CLI failed; install with `npm i -D tailwindcss @tailwindcss/cli` or fix tailwindInput/tailwindContent in manifest.prerender.');
537
+ if (r.stderr) console.error(r.stderr);
538
+ if (r.stdout) console.error(r.stdout);
539
+ return false;
540
+ }
541
+ if (!existsSync(outCss)) {
542
+ console.error('prerender: Tailwind CLI did not produce prerender.tailwind.css');
543
+ return false;
544
+ }
545
+ process.stdout.write(`prerender: wrote ${relative(rootDir, outCss)}\n`);
546
+ return true;
547
+ }
548
+
549
+ function mergeUtilityCssBlocks(allBlocks) {
550
+ const critical = [];
551
+ const main = [];
552
+ const seenC = new Set();
553
+ const seenM = new Set();
554
+ for (const b of allBlocks) {
555
+ if (b.kind === 'critical') {
556
+ if (!seenC.has(b.css)) {
557
+ seenC.add(b.css);
558
+ critical.push(b.css);
559
+ }
560
+ } else {
561
+ if (!seenM.has(b.css)) {
562
+ seenM.add(b.css);
563
+ main.push(b.css);
564
+ }
565
+ }
566
+ }
567
+ const parts = [];
568
+ if (critical.length) parts.push('/* manifest utilities: critical */\n', critical.join('\n\n'));
569
+ if (main.length) parts.push('/* manifest utilities */\n', main.join('\n\n'));
570
+ return parts.join('\n');
571
+ }
572
+
573
+ function walkHtmlFiles(dir, out = []) {
574
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
575
+ if (ent.name.startsWith('.')) continue;
576
+ const p = join(dir, ent.name);
577
+ if (ent.isDirectory()) {
578
+ if (ent.name === 'node_modules') continue;
579
+ walkHtmlFiles(p, out);
580
+ } else if (ent.name.endsWith('.html')) out.push(p);
581
+ }
582
+ return out;
583
+ }
584
+
585
+ function depthFromOutputRoot(outputDir, filePath) {
586
+ const rel = relative(outputDir, dirname(filePath));
587
+ if (!rel || rel === '.') return 0;
588
+ return rel.split(sep).filter(Boolean).length;
589
+ }
590
+
591
+ /** Inject stylesheet link with correct relative href for static hosting (after prerender wrote files). */
592
+ function postProcessInjectStylesheetLink(outputDir, filename) {
593
+ const cssPath = join(outputDir, filename);
594
+ if (!existsSync(cssPath)) return;
595
+ const stat = statSync(cssPath);
596
+ if (stat.size === 0) return;
597
+
598
+ const files = walkHtmlFiles(outputDir);
599
+ for (const file of files) {
600
+ let html = readFileSync(file, 'utf8');
601
+ if (html.includes(filename)) continue;
602
+ const depth = depthFromOutputRoot(outputDir, file);
603
+ const prefix = depth ? '../'.repeat(depth) : '';
604
+ const tag = `<link rel="stylesheet" href="${prefix}${filename}">`;
605
+ html = injectAfterHeadOpen(html, tag);
606
+ writeFileSync(file, html, 'utf8');
607
+ }
608
+ }
609
+
353
610
  // --- (Removed) We used to strip x-text containing product. / feature. to avoid wrong-scope errors
354
611
  // on duplicated x-for output, but that also stripped legitimate loop body bindings (e.g. product
355
612
  // search results), breaking reactivity. If "product/feature is not defined" appears again, fix
@@ -771,6 +1028,7 @@ function copyProjectIntoDist(rootResolved, outputResolved) {
771
1028
 
772
1029
  async function main() {
773
1030
  const config = resolveConfig();
1031
+ const startedAt = Date.now();
774
1032
  let staticServer = null;
775
1033
  if (config.serve) {
776
1034
  const { server, url } = await startStaticServer(config.root);
@@ -784,6 +1042,8 @@ async function main() {
784
1042
  await new Promise((res) => staticServer.close(res));
785
1043
  }
786
1044
  }
1045
+ const secs = ((Date.now() - startedAt) / 1000).toFixed(1);
1046
+ process.stdout.write(`prerender: total time ${secs}s\n`);
787
1047
  }
788
1048
 
789
1049
  async function runPrerender(config) {
@@ -802,6 +1062,12 @@ async function runPrerender(config) {
802
1062
 
803
1063
  const defaultLocale = locales[0] ?? null;
804
1064
  const routeSegments = discoverRoutes(manifest, config.root);
1065
+ const localeSet = new Set(locales.map((l) => String(l).toLowerCase()));
1066
+ const localeNeutralSegments = routeSegments.filter((seg) => {
1067
+ if (!seg) return true;
1068
+ const first = seg.split('/')[0].toLowerCase();
1069
+ return !localeSet.has(first);
1070
+ });
805
1071
  const paths = new Set();
806
1072
  paths.add('');
807
1073
 
@@ -810,14 +1076,15 @@ async function runPrerender(config) {
810
1076
  }
811
1077
  for (const locale of locales.slice(1)) {
812
1078
  paths.add(locale);
813
- for (const seg of routeSegments) {
1079
+ for (const seg of localeNeutralSegments) {
1080
+ if (!seg) continue;
814
1081
  paths.add(`${locale}/${seg}`);
815
1082
  }
816
1083
  }
817
1084
  // Default locale also under its slug (e.g. /en/, /en/page-1) so linking is symmetric; canonical points to root
818
1085
  if (defaultLocale) {
819
1086
  paths.add(defaultLocale);
820
- for (const seg of routeSegments) {
1087
+ for (const seg of localeNeutralSegments) {
821
1088
  if (seg !== '') paths.add(`${defaultLocale}/${seg}`);
822
1089
  }
823
1090
  }
@@ -846,6 +1113,11 @@ async function runPrerender(config) {
846
1113
  mkdirSync(outputResolved, { recursive: true });
847
1114
  copyProjectIntoDist(rootResolved, outputResolved);
848
1115
 
1116
+ const pre = manifest.prerender ?? {};
1117
+ const bundleUtilities = pre.utilitiesBundle !== false;
1118
+ const tailwindBuilt = runTailwindCliForPrerender(rootResolved, outputResolved, pre);
1119
+ const utilityBlocks = [];
1120
+
849
1121
  let browser;
850
1122
  try {
851
1123
  const chromium = await importFromProject('@sparticuz/chromium');
@@ -990,7 +1262,11 @@ async function runPrerender(config) {
990
1262
  xFor.includes('$url') || xFor.includes('$auth') ||
991
1263
  /\bin\s+(filtered\w*|results|searchResults)\b/.test(xFor);
992
1264
  const forceCollapse = explicit || inferred;
993
- if (!forceCollapse) return; // keep prerendered list for SEO
1265
+ if (!forceCollapse) {
1266
+ tpl.removeAttribute('data-prerender-collapsed');
1267
+ return; // keep prerendered list for SEO
1268
+ }
1269
+ tpl.setAttribute('data-prerender-collapsed', '1');
994
1270
  const first = tpl.content?.firstElementChild;
995
1271
  if (!first) return;
996
1272
  const tag = first.tagName;
@@ -1028,7 +1304,9 @@ async function runPrerender(config) {
1028
1304
  return false;
1029
1305
  };
1030
1306
 
1031
- document.querySelectorAll('template[x-for]').forEach((tpl) => {
1307
+ // Only clean up templates we intentionally collapsed above.
1308
+ // Running this on all x-for templates can remove valid prerendered list items.
1309
+ document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
1032
1310
  const xFor = (tpl.getAttribute('x-for') || '').trim();
1033
1311
  const m = xFor.match(loopVarRegex);
1034
1312
  const itemVar = m ? (m[1] || m[3] || '') : '';
@@ -1076,6 +1354,15 @@ async function runPrerender(config) {
1076
1354
  let html = await page.evaluate(() => document.documentElement.outerHTML);
1077
1355
  html = stripDevOnlyContent(html);
1078
1356
  html = stripInjectedPluginScripts(html);
1357
+ html = stripRuntimeTailwindArtifacts(html);
1358
+ if (bundleUtilities) {
1359
+ const extracted = extractUtilityStyleBlocks(html);
1360
+ html = extracted.html;
1361
+ for (const b of extracted.blocks) utilityBlocks.push(b);
1362
+ }
1363
+ if (tailwindBuilt) {
1364
+ html = injectAfterHeadOpen(html, '<link rel="stylesheet" href="/prerender.tailwind.css">');
1365
+ }
1079
1366
  html = stripDuplicatedLoopDirectives(html);
1080
1367
  html = stripPrerenderedXDataDirectives(html);
1081
1368
  const currentLocale =
@@ -1123,6 +1410,15 @@ async function runPrerender(config) {
1123
1410
  await browser.close();
1124
1411
  }
1125
1412
 
1413
+ if (bundleUtilities) {
1414
+ const utilMerged = mergeUtilityCssBlocks(utilityBlocks);
1415
+ if (utilMerged.trim()) {
1416
+ writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
1417
+ process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
1418
+ postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css');
1419
+ }
1420
+ }
1421
+
1126
1422
  writeSeoFiles(
1127
1423
  config.output,
1128
1424
  pathList.filter((p) => p !== NOT_FOUND_PATH),
@@ -1130,6 +1426,7 @@ async function runPrerender(config) {
1130
1426
  locales,
1131
1427
  defaultLocale
1132
1428
  );
1429
+ validatePrerenderedOutput(config.output, pathList.filter((p) => p !== NOT_FOUND_PATH));
1133
1430
 
1134
1431
  if (config.redirects.length > 0) {
1135
1432
  const lines = config.redirects.map((r) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,4 +32,4 @@
32
32
  "url": "git+https://github.com/andrewmatlock/Manifest.git",
33
33
  "directory": "packages/render"
34
34
  }
35
- }
35
+ }