mnfst-render 0.4.3 → 0.4.5
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 +114 -18
- 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),
|
|
@@ -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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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') {
|
|
1140
|
-
|
|
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
|
|
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;
|
|
@@ -2101,6 +2153,48 @@ async function runPrerender(config) {
|
|
|
2101
2153
|
pushDebug({ path: displayPath, stage: 'post-xfor-mark', metrics: afterMark });
|
|
2102
2154
|
}
|
|
2103
2155
|
|
|
2156
|
+
// For static x-for clones that contain data-hydrate elements, inject the loop-scope
|
|
2157
|
+
// variable as x-data on the clone element itself. This ensures that after the loop
|
|
2158
|
+
// template is removed, data-hydrate bindings referencing loop variables (e.g.
|
|
2159
|
+
// plan?.price?.[currency]?.[frequency]) continue to work at runtime via the injected scope.
|
|
2160
|
+
// The parent Alpine scope (e.g. <main x-data="{ currency, frequency }") remains accessible.
|
|
2161
|
+
await page.evaluate(() => {
|
|
2162
|
+
const loopVarRx = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
2163
|
+
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
2164
|
+
if (tpl.hasAttribute('data-prerender-hydrate') || tpl.closest('[data-prerender-hydrate]')) return;
|
|
2165
|
+
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
2166
|
+
const m = xFor.match(loopVarRx);
|
|
2167
|
+
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
2168
|
+
if (!itemVar) return;
|
|
2169
|
+
const first = tpl.content && tpl.content.firstElementChild;
|
|
2170
|
+
if (!first) return;
|
|
2171
|
+
const tag = first.tagName;
|
|
2172
|
+
const cls = first.getAttribute('class') || '';
|
|
2173
|
+
let n = tpl.nextElementSibling;
|
|
2174
|
+
while (n && n.tagName === tag && (n.getAttribute('class') || '') === cls) {
|
|
2175
|
+
// Only process clones that contain data-hydrate descendants
|
|
2176
|
+
if (
|
|
2177
|
+
!n.hasAttribute('x-data') &&
|
|
2178
|
+
(n.hasAttribute('data-prerender-hydrate') || n.querySelector('[data-prerender-hydrate]'))
|
|
2179
|
+
) {
|
|
2180
|
+
try {
|
|
2181
|
+
const A = window.Alpine;
|
|
2182
|
+
if (A && typeof A.$data === 'function') {
|
|
2183
|
+
const scope = A.$data(n);
|
|
2184
|
+
if (scope && Object.prototype.hasOwnProperty.call(scope, itemVar)) {
|
|
2185
|
+
const raw = scope[itemVar];
|
|
2186
|
+
// Serialize only own-enumerable properties to avoid circular refs / proxies
|
|
2187
|
+
const snapshot = JSON.parse(JSON.stringify(raw));
|
|
2188
|
+
n.setAttribute('x-data', JSON.stringify({ [itemVar]: snapshot }));
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
} catch { /* serialisation failed — leave binding as-is */ }
|
|
2192
|
+
}
|
|
2193
|
+
n = n.nextElementSibling;
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2104
2198
|
// Strip loop-scope bindings from x-for clones while <template> nodes still exist.
|
|
2105
2199
|
// (If we remove static templates first, querySelectorAll('template[x-for]') misses them and clones
|
|
2106
2200
|
// keep x-text/x-bind referencing card/item — Alpine then mutates or errors on the static HTML.)
|
|
@@ -2112,6 +2206,8 @@ async function runPrerender(config) {
|
|
|
2112
2206
|
const stripLoopBindings = (el, itemVar, indexVar) => {
|
|
2113
2207
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
2114
2208
|
for (const node of nodes) {
|
|
2209
|
+
// Skip elements inside data-hydrate islands — their bindings must remain live
|
|
2210
|
+
if (node.hasAttribute('data-prerender-hydrate') || node.closest('[data-prerender-hydrate]')) continue;
|
|
2115
2211
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
2116
2212
|
for (const attr of attrs) {
|
|
2117
2213
|
if (!bindingAttrRegex.test(attr.name)) continue;
|