mnfst-render 0.4.3 → 0.4.4

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 +72 -18
  2. package/package.json +1 -1
@@ -238,6 +238,11 @@ function resolveConfig() {
238
238
  localeSubstitutionExclude: Array.isArray(pre.localeSubstitutionExclude)
239
239
  ? pre.localeSubstitutionExclude.map(String)
240
240
  : [],
241
+ /** Explicit locale-neutral paths to render in addition to those discovered automatically.
242
+ * Each entry is expanded to all locale variants (e.g. "legal/privacy" → "cs/legal/privacy", ...) */
243
+ paths: Array.isArray(pre.paths)
244
+ ? pre.paths.map((p) => String(p).replace(/^\/+|\/+$/g, '')).filter(Boolean)
245
+ : [],
241
246
  dryRun: !!cli.dryRun,
242
247
  debugPrerender: !!(cli.debugPrerender ?? pre.debugPrerender),
243
248
  pipelineTimeout: Math.max(3000, Number(pre.pipelineTimeout) || 25000),
@@ -1127,32 +1132,71 @@ function loadAllLocaleContentData(manifest, rootDir, locales) {
1127
1132
  const data = manifest?.data;
1128
1133
  if (!data || typeof data !== 'object') return new Map();
1129
1134
 
1130
- const csvFiles = [];
1131
- const seen = new Set();
1132
- const addCsv = (ref) => {
1133
- if (typeof ref !== 'string' || !ref.endsWith('.csv')) return;
1134
- const p = join(rootDir, ref.startsWith('/') ? ref.slice(1) : ref);
1135
- if (!seen.has(p)) { seen.add(p); csvFiles.push(p); }
1136
- };
1135
+ // Lazy-load js-yaml for parsing per-locale YAML files
1136
+ let jsYaml = null;
1137
+ try { jsYaml = require('js-yaml'); } catch { /* yaml not available; YAML locale files will be skipped */ }
1138
+
1139
+ // Deep-merge source into target (for combining multiple data sources per locale)
1140
+ function deepMerge(target, source) {
1141
+ if (!source || typeof source !== 'object' || Array.isArray(source)) return;
1142
+ for (const key of Object.keys(source)) {
1143
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
1144
+ target[key] = (target[key] && typeof target[key] === 'object') ? target[key] : {};
1145
+ deepMerge(target[key], source[key]);
1146
+ } else {
1147
+ target[key] = source[key];
1148
+ }
1149
+ }
1150
+ }
1151
+
1152
+ const result = new Map();
1153
+ for (const locale of locales) result.set(locale, {});
1137
1154
 
1138
1155
  for (const [, value] of Object.entries(data)) {
1139
- if (typeof value === 'string') { addCsv(value); continue; }
1140
- if (value && typeof value === 'object' && !Array.isArray(value)) {
1156
+ if (typeof value === 'string') {
1157
+ // Single CSV with locale columns (all locales in one file)
1158
+ if (value.endsWith('.csv')) {
1159
+ const csvPath = join(rootDir, value.startsWith('/') ? value.slice(1) : value);
1160
+ for (const locale of locales) {
1161
+ deepMerge(result.get(locale), parseCsvToKeyValue(csvPath, locale));
1162
+ }
1163
+ }
1164
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
1141
1165
  if (value.locales) {
1166
+ // { locales: "/path/to/multi-locale.csv" } format
1142
1167
  const refs = Array.isArray(value.locales) ? value.locales : [value.locales];
1143
- refs.forEach(addCsv);
1168
+ for (const ref of refs) {
1169
+ if (typeof ref !== 'string' || !ref.endsWith('.csv')) continue;
1170
+ const csvPath = join(rootDir, ref.startsWith('/') ? ref.slice(1) : ref);
1171
+ for (const locale of locales) {
1172
+ deepMerge(result.get(locale), parseCsvToKeyValue(csvPath, locale));
1173
+ }
1174
+ }
1175
+ } else {
1176
+ // Per-locale files: { "en": "/data/content.en.yaml", "fr": "/data/content.fr.yaml", ... }
1177
+ for (const [localeKey, filePath] of Object.entries(value)) {
1178
+ if (!locales.includes(localeKey) || typeof filePath !== 'string') continue;
1179
+ const fullPath = join(rootDir, filePath.startsWith('/') ? filePath.slice(1) : filePath);
1180
+ if (!existsSync(fullPath)) continue;
1181
+ let localeData = null;
1182
+ try {
1183
+ const raw = readFileSync(fullPath, 'utf8');
1184
+ if ((filePath.endsWith('.yaml') || filePath.endsWith('.yml')) && jsYaml) {
1185
+ localeData = jsYaml.load(raw);
1186
+ } else if (filePath.endsWith('.json')) {
1187
+ localeData = JSON.parse(raw);
1188
+ } else if (filePath.endsWith('.csv')) {
1189
+ localeData = parseCsvToKeyValue(fullPath, localeKey);
1190
+ }
1191
+ } catch { /* ignore parse errors for individual locale files */ }
1192
+ if (localeData && typeof localeData === 'object') {
1193
+ deepMerge(result.get(localeKey), localeData);
1194
+ }
1195
+ }
1144
1196
  }
1145
1197
  }
1146
1198
  }
1147
1199
 
1148
- const result = new Map();
1149
- for (const locale of locales) {
1150
- const merged = {};
1151
- for (const csvPath of csvFiles) {
1152
- Object.assign(merged, parseCsvToKeyValue(csvPath, locale));
1153
- }
1154
- result.set(locale, merged);
1155
- }
1156
1200
  return result;
1157
1201
  }
1158
1202
 
@@ -1599,6 +1643,14 @@ async function runPrerender(config) {
1599
1643
 
1600
1644
  const defaultLocale = locales[0] ?? null;
1601
1645
  const routeSegments = discoverRoutes(manifest, config.root);
1646
+ // Merge any explicitly configured paths (manifest.prerender.paths) into the discovered segments.
1647
+ // These are treated as locale-neutral and get full locale-expansion like all other discovered paths.
1648
+ if (config.paths && config.paths.length > 0) {
1649
+ const segSet = new Set(routeSegments);
1650
+ for (const p of config.paths) {
1651
+ if (!segSet.has(p)) { routeSegments.push(p); segSet.add(p); }
1652
+ }
1653
+ }
1602
1654
  const localeSet = new Set(locales.map((l) => String(l).toLowerCase()));
1603
1655
  const localeNeutralSegments = routeSegments.filter((seg) => {
1604
1656
  if (!seg) return true;
@@ -2112,6 +2164,8 @@ async function runPrerender(config) {
2112
2164
  const stripLoopBindings = (el, itemVar, indexVar) => {
2113
2165
  const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
2114
2166
  for (const node of nodes) {
2167
+ // Skip elements inside data-hydrate islands — their bindings must remain live
2168
+ if (node.hasAttribute('data-prerender-hydrate') || node.closest('[data-prerender-hydrate]')) continue;
2115
2169
  const attrs = node.attributes ? Array.from(node.attributes) : [];
2116
2170
  for (const attr of attrs) {
2117
2171
  if (!bindingAttrRegex.test(attr.name)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {