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.
- package/manifest.render.mjs +318 -21
- package/package.json +2 -2
package/manifest.render.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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)
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|