nodality 1.0.165 → 1.0.167
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/bin/nodality.js +279 -8
- package/dist/bundle.umd.js +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base-2.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/box.js +1 -1
- package/layout/button.js +1 -1
- package/layout/cards.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/clean-row.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/custom.js +1 -1
- package/layout/div-image.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/dropdown.js +1 -1
- package/layout/empty-element.js +1 -1
- package/layout/external-stylesheet.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/footer.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-new.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/group.js +1 -1
- package/layout/header.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image-old.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +14 -2
- package/layout/label.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list-OLD.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/modernwrap.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/multiswitcherBeta.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/navBar-OLD.js +1 -1
- package/layout/new-flat-adder.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/offset-container.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/saved-new-nav-bar.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/styler.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +1 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/without-new.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +1 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/scroll-video.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/package.json +1 -1
package/bin/nodality.js
CHANGED
|
@@ -40,6 +40,9 @@ function showUsage() {
|
|
|
40
40
|
console.log(`Usage:
|
|
41
41
|
nodality prerender [flags] # SSG: render upload/*.html in place
|
|
42
42
|
nodality compile [src/<file>.js] [flags] # Emit Designer output as a companion file
|
|
43
|
+
nodality fanout # Generate per-item pages from JSON (reads
|
|
44
|
+
# the \`fanout\` block in nodality.config.json;
|
|
45
|
+
# runs automatically before \`prerender\`)
|
|
43
46
|
nodality help
|
|
44
47
|
|
|
45
48
|
Compile flags:
|
|
@@ -162,6 +165,218 @@ function loadConfigFile(cwd) {
|
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
// ─── Fanout (data-driven page expansion) ────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate one HTML + entry-script pair per item in a JSON dataset.
|
|
172
|
+
* The classic use case is a "detail" template that takes a query
|
|
173
|
+
* parameter at runtime (`/detail.html?id=helix`) and the SEO problem
|
|
174
|
+
* that crawlers see an empty mount for every product. Fanout reads
|
|
175
|
+
* the data once at build time, walks an array within it, and writes
|
|
176
|
+
* a per-item static page that the prerender step then turns into
|
|
177
|
+
* fully populated HTML.
|
|
178
|
+
*
|
|
179
|
+
* Configured in `nodality.config.json`:
|
|
180
|
+
*
|
|
181
|
+
* "fanout": [
|
|
182
|
+
* {
|
|
183
|
+
* "template": "detail.html",
|
|
184
|
+
* "data": "products.json",
|
|
185
|
+
* "items": "categories[].products", // dot path with [] for flatMap
|
|
186
|
+
* "id": "id", // field on each item
|
|
187
|
+
* "title": "SLS3 — {name}", // {name} = item.name, {id} etc.
|
|
188
|
+
* "entry": "pages/detail.js", // exported renderDetailPage(id)
|
|
189
|
+
* "bodyAttr": "data-product-id" // attr on <body> for the id
|
|
190
|
+
* }
|
|
191
|
+
* ]
|
|
192
|
+
*
|
|
193
|
+
* Output:
|
|
194
|
+
* • `upload/<basename>-<id>.html` — clone of the template, with
|
|
195
|
+
* `<title>` substituted and `<body data-...="<id>">`.
|
|
196
|
+
* • `upload/pages/<basename>-<id>.js` — thin wrapper that imports
|
|
197
|
+
* the entry's exported renderer and invokes it with this id.
|
|
198
|
+
*
|
|
199
|
+
* Stale outputs from a previous run (orphaned products etc.) are
|
|
200
|
+
* cleaned up before regenerating. The expansion runs once at the
|
|
201
|
+
* start of `nodality prerender` whenever the config has a `fanout`
|
|
202
|
+
* block; you can also invoke it standalone via `nodality fanout`.
|
|
203
|
+
*/
|
|
204
|
+
function runFanout(cwd, uploadDir, fanoutConfig) {
|
|
205
|
+
if (!Array.isArray(fanoutConfig) || fanoutConfig.length === 0) return 0;
|
|
206
|
+
|
|
207
|
+
let totalWrote = 0;
|
|
208
|
+
for (const spec of fanoutConfig) {
|
|
209
|
+
const { template, data, items, id = "id", title, entry, bodyAttr = "data-product-id" } = spec;
|
|
210
|
+
if (!template || !data) {
|
|
211
|
+
console.warn(`[nodality] fanout: skipping spec without template/data`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const tplPath = path.join(uploadDir, template);
|
|
216
|
+
const dataPath = path.join(uploadDir, data);
|
|
217
|
+
if (!fs.existsSync(tplPath)) {
|
|
218
|
+
console.error(`[nodality] fanout: template missing: ${template}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
if (!fs.existsSync(dataPath)) {
|
|
222
|
+
console.error(`[nodality] fanout: data missing: ${data}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const json = JSON.parse(fs.readFileSync(dataPath, "utf8"));
|
|
227
|
+
const list = resolveItemsPath(json, items ?? "");
|
|
228
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
229
|
+
console.warn(`[nodality] fanout: ${data} produced no items at path "${items}"`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const templateHtml = fs.readFileSync(tplPath, "utf8");
|
|
234
|
+
const base = path.basename(template, ".html");
|
|
235
|
+
const pagesDir = path.join(uploadDir, "pages");
|
|
236
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
237
|
+
|
|
238
|
+
// Clean stale outputs from a previous run so removing an item
|
|
239
|
+
// also removes its page. Match by prefix; original template
|
|
240
|
+
// (e.g. `detail.html` itself) is never touched.
|
|
241
|
+
for (const f of fs.readdirSync(uploadDir)) {
|
|
242
|
+
if (new RegExp(`^${escapeRegex(base)}-.+\\.html$`).test(f)) {
|
|
243
|
+
fs.unlinkSync(path.join(uploadDir, f));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const f of fs.readdirSync(pagesDir)) {
|
|
247
|
+
if (new RegExp(`^${escapeRegex(base)}-.+\\.js$`).test(f)) {
|
|
248
|
+
fs.unlinkSync(path.join(pagesDir, f));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Discover the entry file to mirror per-item if not given.
|
|
253
|
+
const entryRel = entry ?? `pages/${base}.js`;
|
|
254
|
+
const entryFull = path.join(uploadDir, entryRel);
|
|
255
|
+
if (!fs.existsSync(entryFull)) {
|
|
256
|
+
console.error(`[nodality] fanout: entry script missing: ${entryRel}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const entryBase = path.basename(entryRel, ".js");
|
|
260
|
+
const entryDir = path.dirname(entryRel);
|
|
261
|
+
|
|
262
|
+
let wrote = 0;
|
|
263
|
+
for (const item of list) {
|
|
264
|
+
const itemId = item?.[id];
|
|
265
|
+
if (!itemId || !/^[A-Za-z0-9][A-Za-z0-9-_]*$/.test(String(itemId))) {
|
|
266
|
+
console.warn(`[nodality] fanout: skipping item with bad ${id}: ${JSON.stringify(itemId)}`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// HTML: rewrite <title>, inject body data-attr, point script src
|
|
271
|
+
// at the per-item entry.
|
|
272
|
+
const resolvedTitle = title
|
|
273
|
+
? interpolate(title, item)
|
|
274
|
+
: String(item?.name ?? itemId);
|
|
275
|
+
const escTitle = escapeHtml(resolvedTitle);
|
|
276
|
+
|
|
277
|
+
let html = templateHtml
|
|
278
|
+
.replace(/<title>[^<]*<\/title>/i, `<title>${escTitle}</title>`)
|
|
279
|
+
.replace(/<body(\s[^>]*)?>/i, (_, attrs = "") => {
|
|
280
|
+
const cleaned = (attrs ?? "").replace(
|
|
281
|
+
new RegExp(`\\s+${escapeRegex(bodyAttr)}="[^"]*"`),
|
|
282
|
+
"",
|
|
283
|
+
);
|
|
284
|
+
return `<body${cleaned} ${bodyAttr}="${itemId}">`;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Rewrite the original `<script src=".../pages/<entryBase>.js">`
|
|
288
|
+
// to point at the per-item wrapper.
|
|
289
|
+
const srcRx = new RegExp(
|
|
290
|
+
`(<script[^>]*src=")([^"]*\\/?)${escapeRegex(entryBase)}\\.js("[^>]*>)`,
|
|
291
|
+
"i",
|
|
292
|
+
);
|
|
293
|
+
html = html.replace(srcRx, `$1$2${entryBase}-${itemId}.js$3`);
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync(path.join(uploadDir, `${base}-${itemId}.html`), html);
|
|
296
|
+
|
|
297
|
+
// JS wrapper. The user's entry must export an async function
|
|
298
|
+
// named `renderDetailPage` (or, generically, the camelCase
|
|
299
|
+
// form of the basename + "Page"). We call it with the id.
|
|
300
|
+
const fnName = entryBase
|
|
301
|
+
.split(/[-_]/)
|
|
302
|
+
.map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
303
|
+
.join("") + "Page"; // detail → renderDetailPage? No — detailPage
|
|
304
|
+
// To minimise convention surprises, default to the underscored
|
|
305
|
+
// `render<Pascal>Page` form too. Try both: the wrapper imports
|
|
306
|
+
// whichever the entry exports.
|
|
307
|
+
const pascal = entryBase
|
|
308
|
+
.split(/[-_]/)
|
|
309
|
+
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
310
|
+
.join("");
|
|
311
|
+
const wrapperBody = `// Auto-generated by \`nodality fanout\`.
|
|
312
|
+
// Per-item entry — invokes the renderer in ${entryRel}
|
|
313
|
+
// with this item's id baked in.
|
|
314
|
+
import * as mod from "./${path.basename(entryRel)}";
|
|
315
|
+
|
|
316
|
+
const fn = mod.render${pascal}Page ?? mod.${fnName} ?? mod.default;
|
|
317
|
+
if (typeof fn !== "function") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"[fanout] ${entryRel} must export render${pascal}Page(id) " +
|
|
320
|
+
"(or a default async function). nodality fanout could not find one.",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
await fn(${JSON.stringify(String(itemId))});
|
|
324
|
+
`;
|
|
325
|
+
fs.writeFileSync(
|
|
326
|
+
path.join(pagesDir, `${entryBase}-${itemId}.js`),
|
|
327
|
+
wrapperBody,
|
|
328
|
+
);
|
|
329
|
+
wrote++;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log(`[nodality] fanout: ${template} × ${data} → ${wrote} page(s)`);
|
|
333
|
+
totalWrote += wrote;
|
|
334
|
+
}
|
|
335
|
+
return totalWrote;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Resolve a dot-path with `[]` segments to an array of items. */
|
|
339
|
+
function resolveItemsPath(data, path) {
|
|
340
|
+
if (!path) return Array.isArray(data) ? data : [];
|
|
341
|
+
const tokens = path.split(/\.|(\[\])/).filter(Boolean);
|
|
342
|
+
let acc = data;
|
|
343
|
+
for (const tok of tokens) {
|
|
344
|
+
if (tok === "[]") {
|
|
345
|
+
if (!Array.isArray(acc)) return [];
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (Array.isArray(acc)) {
|
|
349
|
+
acc = acc.flatMap((x) => (x != null ? [x[tok]] : [])).filter((x) => x != null);
|
|
350
|
+
// After accessing a field through an array, the result is an
|
|
351
|
+
// array of values (possibly nested). Don't auto-flatten — the
|
|
352
|
+
// user can put another `[]` after if they want.
|
|
353
|
+
} else if (acc != null) {
|
|
354
|
+
acc = acc[tok];
|
|
355
|
+
} else {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return Array.isArray(acc) ? acc.flat(Infinity).filter((x) => x != null) : [];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Replace `{field}` in a template with item[field]. */
|
|
363
|
+
function interpolate(template, item) {
|
|
364
|
+
return template.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
365
|
+
const v = key.split(".").reduce((acc, k) => (acc == null ? acc : acc[k]), item);
|
|
366
|
+
return v == null ? "" : String(v);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function escapeRegex(s) {
|
|
371
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function escapeHtml(s) {
|
|
375
|
+
return String(s).replace(/[<>&"']/g, (c) =>
|
|
376
|
+
c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c === '"' ? """ : "'",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
165
380
|
// ─── Page auto-discovery ────────────────────────────────────────
|
|
166
381
|
|
|
167
382
|
/**
|
|
@@ -258,6 +473,16 @@ async function runPrerender(rawArgs) {
|
|
|
258
473
|
);
|
|
259
474
|
}
|
|
260
475
|
|
|
476
|
+
// Data-driven page fanout. When the config declares a `fanout`
|
|
477
|
+
// block (per-product detail pages, per-article blog entries, etc.),
|
|
478
|
+
// expand it BEFORE auto-discovery so the generated HTMLs are picked
|
|
479
|
+
// up alongside the hand-written ones. Subprocess child renders skip
|
|
480
|
+
// fanout — the parent already wrote the files and re-running inside
|
|
481
|
+
// each locale would just thrash them.
|
|
482
|
+
if (!process.env.NODALITY_SSG_LOCALE && Array.isArray(fileConfig.fanout)) {
|
|
483
|
+
runFanout(cwd, uploadDir, fileConfig.fanout);
|
|
484
|
+
}
|
|
485
|
+
|
|
261
486
|
// Explicit `pages` from config wins over auto-discovery. The
|
|
262
487
|
// discovery rules (pages/<base>.js, <base>.js) can't infer
|
|
263
488
|
// irregular pairs like h7-nodality's `index.html → app.js`; for
|
|
@@ -386,6 +611,34 @@ async function runCompile(rawArgs) {
|
|
|
386
611
|
.map((line) => `${line};`)
|
|
387
612
|
.join("\n\n");
|
|
388
613
|
|
|
614
|
+
// Derive the import list from the actual class names used in the
|
|
615
|
+
// emitted statements rather than dumping a hardcoded superset.
|
|
616
|
+
// nodality only exports a known subset of class names; importing a
|
|
617
|
+
// non-exported name (e.g. `Form`, `Polygon`) trips ESM's named-export
|
|
618
|
+
// verification at parse time and breaks the file.
|
|
619
|
+
const NODALITY_EXPORTS = new Set([
|
|
620
|
+
"ElementMapper", "Animator",
|
|
621
|
+
"Text", "Image", "Link", "FlexRow", "UINavBar",
|
|
622
|
+
"Free", "NAudio", "Progress", "Center", "Code",
|
|
623
|
+
"Stack", "Wrapper", "Svg", "MetaAdder", "Table",
|
|
624
|
+
"Dropdown", "Modal", "TextField", "Card", "Wrap",
|
|
625
|
+
"FlexGrid", "ZoomCard", "CustomDivRenderer", "SideBar",
|
|
626
|
+
"SideNav", "SimpleBar", "DesktopBar", "MobileBar",
|
|
627
|
+
"Switcher", "Spacer", "HScroller", "Checkbox", "Base",
|
|
628
|
+
"FilePickera", "Picker", "Range", "RadioGroup", "DataList",
|
|
629
|
+
"Button", "Des", "LinkStyler", "CardGen", "KeyframeAnim",
|
|
630
|
+
"TransformAnim", "Stacker", "ScrollVideo", "Theme",
|
|
631
|
+
"AreaSwitcher", "Video", "UList", "Slider",
|
|
632
|
+
"Polygon", "Circle", "FloatingInput", "Form",
|
|
633
|
+
]);
|
|
634
|
+
const used = new Set();
|
|
635
|
+
for (const line of emitted) {
|
|
636
|
+
for (const m of line.matchAll(/\bnew\s+([A-Z]\w*)/g)) {
|
|
637
|
+
if (NODALITY_EXPORTS.has(m[1])) used.add(m[1]);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const importList = [...used].sort().join(", ") || "Text";
|
|
641
|
+
|
|
389
642
|
const out = `// Auto-emitted by \`nodality compile\` from ${srcRel}.
|
|
390
643
|
// This is the imperative form the Designer would have shown in the
|
|
391
644
|
// \`code: true\` panel. It is a throwaway artifact — diff it against
|
|
@@ -393,14 +646,7 @@ async function runCompile(rawArgs) {
|
|
|
393
646
|
// then refine by hand. Re-run \`nodality compile\` whenever you
|
|
394
647
|
// sketch new pieces in ${srcRel}.
|
|
395
648
|
|
|
396
|
-
import {
|
|
397
|
-
Text, Image, Link, FlexRow, FlexGrid, Wrapper, Center, Stack,
|
|
398
|
-
Card, ZoomCard, Switcher, MobileBar, DesktopBar, SideNav, UINavBar,
|
|
399
|
-
Dropdown, Modal, Table, Spacer, HScroller, Polygon, Circle, UList,
|
|
400
|
-
Free, Audio, Progress, Code, MetaAdder, TextField,
|
|
401
|
-
FloatingInput, Range, RadioGroup, Picker, FilePickera, DataList,
|
|
402
|
-
Base, Form, Button, Slider, Video, Checkbox,
|
|
403
|
-
} from "nodality";
|
|
649
|
+
import { ${importList} } from "nodality";
|
|
404
650
|
|
|
405
651
|
${body}
|
|
406
652
|
`;
|
|
@@ -420,6 +666,29 @@ ${body}
|
|
|
420
666
|
}
|
|
421
667
|
}
|
|
422
668
|
|
|
669
|
+
// ─── fanout subcommand (standalone) ────────────────────────────
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Run the fanout expansion standalone (without prerender) — useful
|
|
673
|
+
* for debugging the generated files before kicking off a full SSG.
|
|
674
|
+
* Reads the `fanout` block from nodality.config.json.
|
|
675
|
+
*/
|
|
676
|
+
async function runFanoutStandalone(rawArgs) {
|
|
677
|
+
const cwd = process.cwd();
|
|
678
|
+
const flags = parseFlags(rawArgs);
|
|
679
|
+
const fileConfig = loadConfigFile(cwd);
|
|
680
|
+
const uploadDir = path.resolve(cwd, flags.upload || fileConfig.uploadDir || "upload");
|
|
681
|
+
|
|
682
|
+
if (!Array.isArray(fileConfig.fanout) || fileConfig.fanout.length === 0) {
|
|
683
|
+
console.error(`[nodality] fanout: no \`fanout\` block in nodality.config.json`);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
const wrote = runFanout(cwd, uploadDir, fileConfig.fanout);
|
|
687
|
+
if (wrote === 0) {
|
|
688
|
+
console.warn(`[nodality] fanout: 0 page(s) generated — check config & data`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
423
692
|
// ─── Dispatch ──────────────────────────────────────────────────
|
|
424
693
|
|
|
425
694
|
async function main() {
|
|
@@ -441,6 +710,8 @@ async function main() {
|
|
|
441
710
|
await runPrerender(rest);
|
|
442
711
|
} else if (command === "compile") {
|
|
443
712
|
await runCompile(rest);
|
|
713
|
+
} else if (command === "fanout") {
|
|
714
|
+
await runFanoutStandalone(rest);
|
|
444
715
|
} else {
|
|
445
716
|
console.error(`[nodality] Unknown command: ${command}`);
|
|
446
717
|
showUsage();
|