mnfst-render 0.1.5 → 0.1.6

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 +68 -12
  2. package/package.json +1 -1
@@ -144,16 +144,37 @@ function extractXRouteConditions(html) {
144
144
  return conditions;
145
145
  }
146
146
 
147
+ function normalizeRouteCondition(cond) {
148
+ const raw = String(cond || '').trim();
149
+ if (!raw) return { kind: 'all', path: '' };
150
+ if (raw.startsWith('!')) {
151
+ const omitted = raw.slice(1).trim();
152
+ if (!omitted || omitted === '*') return { kind: 'not-found', path: '' }; // !*
153
+ return { kind: 'omit', path: omitted };
154
+ }
155
+ if (raw === '*') return { kind: 'all', path: '' };
156
+ const withoutExact = raw.startsWith('=') ? raw.slice(1) : raw;
157
+ const trimmed = withoutExact.replace(/^\/+|\/+$/g, '');
158
+ if (!trimmed) return { kind: 'root', path: '' };
159
+ if (trimmed.endsWith('/*')) {
160
+ const base = trimmed.slice(0, -2).replace(/^\/+|\/+$/g, '');
161
+ return base ? { kind: 'wildcard-prefix', path: base } : { kind: 'all', path: '' };
162
+ }
163
+ if (trimmed.includes('*')) return { kind: 'unsupported-pattern', path: trimmed };
164
+ return { kind: 'path', path: trimmed };
165
+ }
166
+
147
167
  function conditionsToPaths(conditions) {
148
168
  const paths = new Set();
149
169
  paths.add('/');
150
170
  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
- }
171
+ const parsed = normalizeRouteCondition(c);
172
+ // Discovery rules aligned with router docs:
173
+ // - "*" and omitted routes do not define concrete paths.
174
+ // - "!*" is handled separately via explicit NOT_FOUND path.
175
+ // - "about/*" does not include "/about" by itself; concrete children come from data paths.
176
+ if (parsed.kind === 'path') paths.add('/' + parsed.path);
177
+ else if (parsed.kind === 'root') paths.add('/');
157
178
  }
158
179
  return paths;
159
180
  }
@@ -177,6 +198,13 @@ function parseYamlPaths(filePath) {
177
198
  const segment = pathMatch[1].trim();
178
199
  paths.push(`${currentGroup}/${segment}`);
179
200
  }
201
+ const genericPathMatch = line.match(/^\s*(?:-\s*)?(?:path|slug):\s*["']?([^"'\n#]+)["']?/);
202
+ if (genericPathMatch) {
203
+ const v = genericPathMatch[1].trim().replace(/^\/+|\/+$/g, '');
204
+ if (v && !v.includes('*') && !/\.[a-z0-9]+$/i.test(v)) {
205
+ paths.push(v);
206
+ }
207
+ }
180
208
  }
181
209
  return paths;
182
210
  }
@@ -309,10 +337,8 @@ function discoverRoutes(manifest, rootDir) {
309
337
  const compPath = join(rootDir, rel);
310
338
  if (existsSync(compPath)) {
311
339
  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
- });
340
+ const conditions = extractXRouteConditions(html);
341
+ conditionsToPaths(conditions).forEach((p) => pathSet.add(p));
316
342
  }
317
343
  }
318
344
 
@@ -329,6 +355,29 @@ function pathToFileSegments(pathname) {
329
355
  return normalized ? normalized.split('/') : [];
330
356
  }
331
357
 
358
+ function validatePrerenderedOutput(outputDir, pathList) {
359
+ const invalidPathTokens = pathList.filter((p) => /(^|\/)[*=]/.test(p) || p.includes('/*') || p.includes('*'));
360
+ if (invalidPathTokens.length > 0) {
361
+ throw new Error(`prerender validation failed: invalid discovered route token(s): ${invalidPathTokens.join(', ')}`);
362
+ }
363
+
364
+ const badFolders = [];
365
+ function walk(dir, rel = '') {
366
+ const entries = readdirSync(dir, { withFileTypes: true });
367
+ for (const ent of entries) {
368
+ if (!ent.isDirectory()) continue;
369
+ const seg = ent.name;
370
+ const nextRel = rel ? `${rel}/${seg}` : seg;
371
+ if (seg.includes('*') || seg.startsWith('=')) badFolders.push(nextRel);
372
+ walk(join(dir, seg), nextRel);
373
+ }
374
+ }
375
+ if (existsSync(outputDir)) walk(outputDir, '');
376
+ if (badFolders.length > 0) {
377
+ throw new Error(`prerender validation failed: invalid output folder(s): ${badFolders.join(', ')}`);
378
+ }
379
+ }
380
+
332
381
  // --- Strip dev-only injected content (e.g. browser-sync) so dist works under any server -
333
382
 
334
383
  function stripDevOnlyContent(html) {
@@ -990,7 +1039,11 @@ async function runPrerender(config) {
990
1039
  xFor.includes('$url') || xFor.includes('$auth') ||
991
1040
  /\bin\s+(filtered\w*|results|searchResults)\b/.test(xFor);
992
1041
  const forceCollapse = explicit || inferred;
993
- if (!forceCollapse) return; // keep prerendered list for SEO
1042
+ if (!forceCollapse) {
1043
+ tpl.removeAttribute('data-prerender-collapsed');
1044
+ return; // keep prerendered list for SEO
1045
+ }
1046
+ tpl.setAttribute('data-prerender-collapsed', '1');
994
1047
  const first = tpl.content?.firstElementChild;
995
1048
  if (!first) return;
996
1049
  const tag = first.tagName;
@@ -1028,7 +1081,9 @@ async function runPrerender(config) {
1028
1081
  return false;
1029
1082
  };
1030
1083
 
1031
- document.querySelectorAll('template[x-for]').forEach((tpl) => {
1084
+ // Only clean up templates we intentionally collapsed above.
1085
+ // Running this on all x-for templates can remove valid prerendered list items.
1086
+ document.querySelectorAll('template[x-for][data-prerender-collapsed="1"]').forEach((tpl) => {
1032
1087
  const xFor = (tpl.getAttribute('x-for') || '').trim();
1033
1088
  const m = xFor.match(loopVarRegex);
1034
1089
  const itemVar = m ? (m[1] || m[3] || '') : '';
@@ -1130,6 +1185,7 @@ async function runPrerender(config) {
1130
1185
  locales,
1131
1186
  defaultLocale
1132
1187
  );
1188
+ validatePrerenderedOutput(config.output, pathList.filter((p) => p !== NOT_FOUND_PATH));
1133
1189
 
1134
1190
  if (config.redirects.length > 0) {
1135
1191
  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.6",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {