mnfst-render 0.4.2 → 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.
- package/manifest.render.mjs +96 -27
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -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),
|
|
@@ -956,6 +961,7 @@ function markPrerenderedManifestComponents(html) {
|
|
|
956
961
|
return html.replace(/<(x-[a-z][\w-]*)([^>]*)>/gi, (full, tag, attrs) => {
|
|
957
962
|
const a = attrs || '';
|
|
958
963
|
if (/\bdata-pre-rendered\s*=/i.test(a) || /\bdata-processed\s*=/i.test(a)) return full;
|
|
964
|
+
if (/\bdata-prerender-hydrate\b/i.test(a)) return full; // Inside data-hydrate island — skip
|
|
959
965
|
const spacer = /\S/.test(a) ? ' ' : '';
|
|
960
966
|
return `<${tag}${a}${spacer}data-pre-rendered="1">`;
|
|
961
967
|
});
|
|
@@ -1126,32 +1132,71 @@ function loadAllLocaleContentData(manifest, rootDir, locales) {
|
|
|
1126
1132
|
const data = manifest?.data;
|
|
1127
1133
|
if (!data || typeof data !== 'object') return new Map();
|
|
1128
1134
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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, {});
|
|
1136
1154
|
|
|
1137
1155
|
for (const [, value] of Object.entries(data)) {
|
|
1138
|
-
if (typeof value === 'string') {
|
|
1139
|
-
|
|
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)) {
|
|
1140
1165
|
if (value.locales) {
|
|
1166
|
+
// { locales: "/path/to/multi-locale.csv" } format
|
|
1141
1167
|
const refs = Array.isArray(value.locales) ? value.locales : [value.locales];
|
|
1142
|
-
refs
|
|
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
|
+
}
|
|
1143
1196
|
}
|
|
1144
1197
|
}
|
|
1145
1198
|
}
|
|
1146
1199
|
|
|
1147
|
-
const result = new Map();
|
|
1148
|
-
for (const locale of locales) {
|
|
1149
|
-
const merged = {};
|
|
1150
|
-
for (const csvPath of csvFiles) {
|
|
1151
|
-
Object.assign(merged, parseCsvToKeyValue(csvPath, locale));
|
|
1152
|
-
}
|
|
1153
|
-
result.set(locale, merged);
|
|
1154
|
-
}
|
|
1155
1200
|
return result;
|
|
1156
1201
|
}
|
|
1157
1202
|
|
|
@@ -1161,14 +1206,24 @@ function loadAllLocaleContentData(manifest, rootDir, locales) {
|
|
|
1161
1206
|
*/
|
|
1162
1207
|
function buildSubstitutionPairs(defaultLocaleData, targetLocaleData) {
|
|
1163
1208
|
const pairs = [];
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1209
|
+
function collectPairs(defaultObj, targetObj) {
|
|
1210
|
+
if (!defaultObj || !targetObj) return;
|
|
1211
|
+
for (const key of Object.keys(defaultObj)) {
|
|
1212
|
+
const defaultVal = defaultObj[key];
|
|
1213
|
+
const targetVal = targetObj[key];
|
|
1214
|
+
if (defaultVal && typeof defaultVal === 'object') {
|
|
1215
|
+
// Recurse into nested objects (produced by setNestedKey for dotted CSV keys)
|
|
1216
|
+
collectPairs(defaultVal, targetVal && typeof targetVal === 'object' ? targetVal : {});
|
|
1217
|
+
} else {
|
|
1218
|
+
const from = String(defaultVal ?? '').trim();
|
|
1219
|
+
const to = String(targetVal ?? '').trim();
|
|
1220
|
+
if (!from || from === to) continue;
|
|
1221
|
+
pairs.push([from, to]);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1171
1224
|
}
|
|
1225
|
+
collectPairs(defaultLocaleData, targetLocaleData);
|
|
1226
|
+
// Sort longest-first so more specific strings are replaced before shorter substrings
|
|
1172
1227
|
pairs.sort((a, b) => b[0].length - a[0].length);
|
|
1173
1228
|
return pairs;
|
|
1174
1229
|
}
|
|
@@ -1266,6 +1321,9 @@ function generateLocaleVariantHtml({
|
|
|
1266
1321
|
html = stripRedundantImgSrcBindings(html);
|
|
1267
1322
|
html = stripEmptyInlineMaskStyles(html);
|
|
1268
1323
|
html = stripResolvedXIconDirectives(html);
|
|
1324
|
+
// markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
|
|
1325
|
+
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
1326
|
+
html = markPrerenderedManifestComponents(html);
|
|
1269
1327
|
html = stripPrerenderHydrateMarkers(html);
|
|
1270
1328
|
|
|
1271
1329
|
const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
|
|
@@ -1290,7 +1348,6 @@ function generateLocaleVariantHtml({
|
|
|
1290
1348
|
'</head>',
|
|
1291
1349
|
`${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${routeMeta}${baseMeta}<meta name="manifest:prerendered" content="1">\n<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`
|
|
1292
1350
|
);
|
|
1293
|
-
html = markPrerenderedManifestComponents(html);
|
|
1294
1351
|
|
|
1295
1352
|
return { html, utilityBlocks: pageUtilityBlocks };
|
|
1296
1353
|
}
|
|
@@ -1586,6 +1643,14 @@ async function runPrerender(config) {
|
|
|
1586
1643
|
|
|
1587
1644
|
const defaultLocale = locales[0] ?? null;
|
|
1588
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
|
+
}
|
|
1589
1654
|
const localeSet = new Set(locales.map((l) => String(l).toLowerCase()));
|
|
1590
1655
|
const localeNeutralSegments = routeSegments.filter((seg) => {
|
|
1591
1656
|
if (!seg) return true;
|
|
@@ -2099,6 +2164,8 @@ async function runPrerender(config) {
|
|
|
2099
2164
|
const stripLoopBindings = (el, itemVar, indexVar) => {
|
|
2100
2165
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
2101
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;
|
|
2102
2169
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
2103
2170
|
for (const attr of attrs) {
|
|
2104
2171
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
@@ -2295,6 +2362,9 @@ async function runPrerender(config) {
|
|
|
2295
2362
|
html = stripRedundantImgSrcBindings(html);
|
|
2296
2363
|
html = stripEmptyInlineMaskStyles(html);
|
|
2297
2364
|
html = stripResolvedXIconDirectives(html);
|
|
2365
|
+
// markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
|
|
2366
|
+
// detect data-prerender-hydrate markers and skip components inside hydrate islands.
|
|
2367
|
+
html = markPrerenderedManifestComponents(html);
|
|
2298
2368
|
html = stripPrerenderHydrateMarkers(html);
|
|
2299
2369
|
html = rewriteHtmlAssetPaths(html, fileSegments.length);
|
|
2300
2370
|
const liveBase = config.liveUrl.replace(/\/$/, '');
|
|
@@ -2314,7 +2384,6 @@ async function runPrerender(config) {
|
|
|
2314
2384
|
'</head>',
|
|
2315
2385
|
`${canonicalHreflang}${injectOgLocale ? ogLocale : ''}${routeMeta}${baseMeta}${prerenderedMeta}<meta name="manifest:router-base-depth" content="${routeDepth}">\n</head>`
|
|
2316
2386
|
);
|
|
2317
|
-
html = markPrerenderedManifestComponents(html);
|
|
2318
2387
|
mkdirSync(outDir, { recursive: true });
|
|
2319
2388
|
writeFileSync(outFile, html, 'utf8');
|
|
2320
2389
|
pushDebug({
|