@jxsuite/compiler 0.9.0 → 0.10.2
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/dist/compiler.js +10 -10
- package/package.json +3 -3
- package/src/compile-cli.js +10 -0
- package/src/compiler.js +27 -32
- package/src/site/content-loader.js +60 -61
- package/src/site/context-injection.js +16 -16
- package/src/site/pages-discovery.js +12 -12
- package/src/site/site-build.js +17 -17
- package/src/site/site-loader.js +2 -2
- package/src/targets/compile-client.js +17 -29
- package/src/targets/compile-server.js +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@apidevtools/json-schema-ref-parser": "^15.3.5",
|
|
40
|
-
"@jxsuite/parser": "^0.
|
|
41
|
-
"@jxsuite/runtime": "^0.
|
|
40
|
+
"@jxsuite/parser": "^0.10.1",
|
|
41
|
+
"@jxsuite/runtime": "^0.10.1",
|
|
42
42
|
"remark-gfm": "^4.0.1",
|
|
43
43
|
"remark-stringify": "^11.0.0",
|
|
44
44
|
"sharp": "^0.34.5",
|
package/src/compiler.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - Server → compile-server.js (Hono server handler)
|
|
11
11
|
*
|
|
12
12
|
* Usage (CLI):
|
|
13
|
-
* bun packages/compiler/
|
|
13
|
+
* bun packages/compiler/src/compile-cli.js <source.json> [output.html]
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
@@ -132,35 +132,30 @@ export async function compile(sourcePath, opts = {}) {
|
|
|
132
132
|
|
|
133
133
|
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
})
|
|
162
|
-
.catch((/** @type {any} */ err) => {
|
|
163
|
-
console.error(err);
|
|
164
|
-
process.exit(1);
|
|
165
|
-
});
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} src
|
|
137
|
+
* @param {string} [out]
|
|
138
|
+
*/
|
|
139
|
+
export async function runCli(src, out) {
|
|
140
|
+
const [result, server] = await Promise.all([compile(src), compileServer(src)]);
|
|
141
|
+
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
142
|
+
const { dirname, join } = await import("node:path");
|
|
143
|
+
if (out) {
|
|
144
|
+
writeFileSync(out, result.html, "utf8");
|
|
145
|
+
console.error(`Written to ${out}`);
|
|
146
|
+
const outDir = dirname(out);
|
|
147
|
+
for (const f of result.files) {
|
|
148
|
+
const filePath = join(outDir, f.path);
|
|
149
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
150
|
+
writeFileSync(filePath, f.content, "utf8");
|
|
151
|
+
console.error(`Written to ${filePath}`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
process.stdout.write(result.html);
|
|
155
|
+
}
|
|
156
|
+
if (server && out) {
|
|
157
|
+
const serverOut = out.replace(/(\.[^.]+)?$/, "-server.js");
|
|
158
|
+
writeFileSync(serverOut, /** @type {string} */ (server), "utf8");
|
|
159
|
+
console.error(`Server handler written to ${serverOut}`);
|
|
160
|
+
}
|
|
166
161
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Content-loader.js — Content
|
|
2
|
+
* Content-loader.js — Content type loader
|
|
3
3
|
*
|
|
4
|
-
* Loads content
|
|
5
|
-
*
|
|
4
|
+
* Loads content types defined in project.json's `contentTypes` key. Supports Markdown (.md), JSON
|
|
5
|
+
* (.json), and CSV (.csv) source files.
|
|
6
6
|
*
|
|
7
7
|
* Phase 2 implementation of site-architecture spec §6.
|
|
8
8
|
*
|
|
@@ -30,12 +30,13 @@ function parseCSV(csv) {
|
|
|
30
30
|
/** @type {string[]} */
|
|
31
31
|
const lines = [];
|
|
32
32
|
|
|
33
|
-
// Split into rows respecting quoted newlines
|
|
33
|
+
// Split into rows respecting quoted newlines (preserve raw characters)
|
|
34
34
|
for (let i = 0; i < csv.length; i++) {
|
|
35
35
|
const ch = csv[i];
|
|
36
36
|
if (ch === '"') {
|
|
37
|
+
current += ch;
|
|
37
38
|
if (inQuotes && csv[i + 1] === '"') {
|
|
38
|
-
current +=
|
|
39
|
+
current += csv[i + 1];
|
|
39
40
|
i++;
|
|
40
41
|
} else {
|
|
41
42
|
inQuotes = !inQuotes;
|
|
@@ -98,7 +99,7 @@ let _mdModule = null;
|
|
|
98
99
|
|
|
99
100
|
/**
|
|
100
101
|
* Lazily import @jxsuite/parser for Markdown support. This avoids hard dependency — only loads when
|
|
101
|
-
* MD
|
|
102
|
+
* MD content types exist.
|
|
102
103
|
*
|
|
103
104
|
* @returns {Promise<any>}
|
|
104
105
|
*/
|
|
@@ -155,7 +156,6 @@ function loadJSONEntries(filePath) {
|
|
|
155
156
|
id: item.id ?? basename(filePath, ".json") + "-" + i,
|
|
156
157
|
data: item,
|
|
157
158
|
body: null,
|
|
158
|
-
rendered: null,
|
|
159
159
|
}));
|
|
160
160
|
}
|
|
161
161
|
// Single object file — filename is the id
|
|
@@ -164,7 +164,6 @@ function loadJSONEntries(filePath) {
|
|
|
164
164
|
id: raw.id ?? basename(filePath, ".json"),
|
|
165
165
|
data: raw,
|
|
166
166
|
body: null,
|
|
167
|
-
rendered: null,
|
|
168
167
|
},
|
|
169
168
|
];
|
|
170
169
|
}
|
|
@@ -173,7 +172,7 @@ function loadJSONEntries(filePath) {
|
|
|
173
172
|
* Load a CSV file into ContentEntry(s).
|
|
174
173
|
*
|
|
175
174
|
* @param {string} filePath - Absolute path to .csv file
|
|
176
|
-
* @param {any} [schema] -
|
|
175
|
+
* @param {any} [schema] - Content type schema (for type coercion)
|
|
177
176
|
* @returns {object[]} Array of ContentEntry shapes
|
|
178
177
|
*/
|
|
179
178
|
function loadCSVEntries(filePath, schema) {
|
|
@@ -192,17 +191,17 @@ function loadCSVEntries(filePath, schema) {
|
|
|
192
191
|
}
|
|
193
192
|
// Use `id` column, `sku` column, or row index as the entry ID
|
|
194
193
|
const id = row.id ?? row.sku ?? String(i);
|
|
195
|
-
return { id, data: row, body: null
|
|
194
|
+
return { id, data: row, body: null };
|
|
196
195
|
});
|
|
197
196
|
}
|
|
198
197
|
|
|
199
198
|
// ─── Content Config ───────────────────────────────────────────────────────────
|
|
200
199
|
|
|
201
200
|
/**
|
|
202
|
-
* Load and parse content
|
|
201
|
+
* Load and parse content types config from project.json.
|
|
203
202
|
*
|
|
204
203
|
* @param {string} projectRoot - Project root directory
|
|
205
|
-
* @param {Record<string, any>} [projectConfig] - Already-loaded project config with `
|
|
204
|
+
* @param {Record<string, any>} [projectConfig] - Already-loaded project config with `contentTypes`
|
|
206
205
|
* key
|
|
207
206
|
* @returns {{ config: any; contentDir: string } | null} Parsed config or null if no content dir
|
|
208
207
|
*/
|
|
@@ -210,68 +209,68 @@ export function loadContentConfig(projectRoot, projectConfig = undefined) {
|
|
|
210
209
|
const contentDir = resolve(projectRoot, "content");
|
|
211
210
|
|
|
212
211
|
/** @type {any} */
|
|
213
|
-
const config = {
|
|
212
|
+
const config = { contentTypes: projectConfig?.contentTypes ?? {} };
|
|
214
213
|
|
|
215
214
|
return { config, contentDir };
|
|
216
215
|
}
|
|
217
216
|
|
|
218
|
-
// ───
|
|
217
|
+
// ─── Content Type Loading ────────────────────────────────────────────────────
|
|
219
218
|
|
|
220
219
|
/**
|
|
221
|
-
* Load all content
|
|
220
|
+
* Load all content types defined in project.json.
|
|
222
221
|
*
|
|
223
222
|
* @param {string} projectRoot - Project root directory
|
|
224
223
|
* @param {Record<string, any>} [projectConfig] - Already-loaded project config
|
|
225
|
-
* @returns {Promise<Map<string, any[]>>} Map of
|
|
224
|
+
* @returns {Promise<Map<string, any[]>>} Map of content type name → array of ContentEntry
|
|
226
225
|
*/
|
|
227
|
-
export async function
|
|
226
|
+
export async function loadContentTypes(projectRoot, projectConfig = undefined) {
|
|
228
227
|
const result = loadContentConfig(projectRoot, projectConfig);
|
|
229
228
|
if (!result) return new Map();
|
|
230
229
|
|
|
231
230
|
const { config } = result;
|
|
232
231
|
/** @type {Map<string, any[]>} */
|
|
233
|
-
const
|
|
232
|
+
const contentTypes = new Map();
|
|
234
233
|
|
|
235
|
-
for (const [name,
|
|
236
|
-
const entries = await
|
|
237
|
-
|
|
234
|
+
for (const [name, contentTypeDef] of Object.entries(config.contentTypes)) {
|
|
235
|
+
const entries = await loadContentType(name, /** @type {any} */ (contentTypeDef), projectRoot);
|
|
236
|
+
contentTypes.set(name, entries);
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
return
|
|
239
|
+
return contentTypes;
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
/**
|
|
244
|
-
* Get the $elements array for a specific
|
|
243
|
+
* Get the $elements array for a specific content type, if defined in project.json contentTypes.
|
|
245
244
|
*
|
|
246
245
|
* @param {string} projectRoot - Project root directory
|
|
247
|
-
* @param {string}
|
|
246
|
+
* @param {string} contentTypeName - Name of the content type
|
|
248
247
|
* @param {Record<string, any>} [projectConfig] - Already-loaded project config
|
|
249
248
|
* @returns {any[] | undefined}
|
|
250
249
|
*/
|
|
251
|
-
export function
|
|
250
|
+
export function getContentTypeElements(projectRoot, contentTypeName, projectConfig = undefined) {
|
|
252
251
|
const result = loadContentConfig(projectRoot, projectConfig);
|
|
253
252
|
if (!result) return undefined;
|
|
254
|
-
const def = result.config.
|
|
253
|
+
const def = result.config.contentTypes?.[contentTypeName];
|
|
255
254
|
return def?.$elements;
|
|
256
255
|
}
|
|
257
256
|
|
|
258
257
|
/**
|
|
259
|
-
* Load a single
|
|
258
|
+
* Load a single content type by its definition.
|
|
260
259
|
*
|
|
261
|
-
* @param {string} name -
|
|
262
|
-
* @param {any}
|
|
260
|
+
* @param {string} name - Content type name
|
|
261
|
+
* @param {any} contentTypeDef - Content type definition from project.json
|
|
263
262
|
* @param {string} projectRoot - Absolute path to project root directory
|
|
264
263
|
* @returns {Promise<any[]>} Array of ContentEntry
|
|
265
264
|
*/
|
|
266
|
-
async function
|
|
267
|
-
const source =
|
|
268
|
-
const schema =
|
|
265
|
+
async function loadContentType(name, contentTypeDef, projectRoot) {
|
|
266
|
+
const source = contentTypeDef.source;
|
|
267
|
+
const schema = contentTypeDef.schema;
|
|
269
268
|
|
|
270
|
-
// Derive directive allowedNames from
|
|
269
|
+
// Derive directive allowedNames from content type $elements (tag names from npm packages)
|
|
271
270
|
/** @type {any} */
|
|
272
|
-
const directiveOptions =
|
|
271
|
+
const directiveOptions = contentTypeDef.$elements?.length
|
|
273
272
|
? {
|
|
274
|
-
allowedNames:
|
|
273
|
+
allowedNames: contentTypeDef.$elements
|
|
275
274
|
.filter((/** @type {any} */ e) => typeof e === "string" || e?.$ref)
|
|
276
275
|
.map((/** @type {any} */ e) => (typeof e === "string" ? e : e.$ref)),
|
|
277
276
|
}
|
|
@@ -307,14 +306,14 @@ async function loadCollection(name, collectionDef, projectRoot) {
|
|
|
307
306
|
// ─── Schema Validation ────────────────────────────────────────────────────────
|
|
308
307
|
|
|
309
308
|
/**
|
|
310
|
-
* Validate content entries against their
|
|
309
|
+
* Validate content entries against their content type schema. Logs warnings for missing required
|
|
311
310
|
* fields and type mismatches.
|
|
312
311
|
*
|
|
313
312
|
* @param {any[]} entries - Array of ContentEntry
|
|
314
|
-
* @param {any} schema - JSON Schema for the
|
|
315
|
-
* @param {string}
|
|
313
|
+
* @param {any} schema - JSON Schema for the content type
|
|
314
|
+
* @param {string} contentTypeName - For error messages
|
|
316
315
|
*/
|
|
317
|
-
function validateEntries(entries, schema,
|
|
316
|
+
function validateEntries(entries, schema, contentTypeName) {
|
|
318
317
|
const required = schema.required ?? [];
|
|
319
318
|
const properties = schema.properties ?? {};
|
|
320
319
|
|
|
@@ -323,7 +322,7 @@ function validateEntries(entries, schema, collectionName) {
|
|
|
323
322
|
for (const field of required) {
|
|
324
323
|
if (!(field in entry.data) || entry.data[field] == null) {
|
|
325
324
|
console.warn(
|
|
326
|
-
`Content validation: "${
|
|
325
|
+
`Content validation: "${contentTypeName}/${entry.id}" missing required field "${field}"`,
|
|
327
326
|
);
|
|
328
327
|
}
|
|
329
328
|
}
|
|
@@ -336,36 +335,36 @@ function validateEntries(entries, schema, collectionName) {
|
|
|
336
335
|
|
|
337
336
|
if (d.type === "string" && typeof value !== "string") {
|
|
338
337
|
console.warn(
|
|
339
|
-
`Content validation: "${
|
|
338
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected string, got ${typeof value}`,
|
|
340
339
|
);
|
|
341
340
|
} else if (d.type === "number" && typeof value !== "number") {
|
|
342
341
|
console.warn(
|
|
343
|
-
`Content validation: "${
|
|
342
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected number, got ${typeof value}`,
|
|
344
343
|
);
|
|
345
344
|
} else if (d.type === "boolean" && typeof value !== "boolean") {
|
|
346
345
|
console.warn(
|
|
347
|
-
`Content validation: "${
|
|
346
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected boolean, got ${typeof value}`,
|
|
348
347
|
);
|
|
349
348
|
} else if (d.type === "array" && !Array.isArray(value)) {
|
|
350
349
|
console.warn(
|
|
351
|
-
`Content validation: "${
|
|
350
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected array, got ${typeof value}`,
|
|
352
351
|
);
|
|
353
352
|
}
|
|
354
353
|
}
|
|
355
354
|
}
|
|
356
355
|
}
|
|
357
356
|
|
|
358
|
-
// ───
|
|
357
|
+
// ─── Content Type Querying ───────────────────────────────────────────────────
|
|
359
358
|
|
|
360
359
|
/**
|
|
361
|
-
* Query a loaded
|
|
360
|
+
* Query a loaded content type with filter, sort, and limit. Implements the ContentCollection
|
|
362
361
|
* $prototype resolution.
|
|
363
362
|
*
|
|
364
|
-
* @param {any[]} entries - Full
|
|
363
|
+
* @param {any[]} entries - Full content type entries
|
|
365
364
|
* @param {any} [query] - Query options
|
|
366
365
|
* @returns {any[]} Filtered, sorted, limited entries
|
|
367
366
|
*/
|
|
368
|
-
export function
|
|
367
|
+
export function queryContentType(entries, query = {}) {
|
|
369
368
|
let result = [...entries];
|
|
370
369
|
|
|
371
370
|
// Filter
|
|
@@ -402,9 +401,9 @@ export function queryCollection(entries, query = {}) {
|
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
/**
|
|
405
|
-
* Find a single entry by ID in a
|
|
404
|
+
* Find a single entry by ID in a content type. Implements the ContentEntry $prototype resolution.
|
|
406
405
|
*
|
|
407
|
-
* @param {any[]} entries - Full
|
|
406
|
+
* @param {any[]} entries - Full content type entries
|
|
408
407
|
* @param {string} id - Entry ID to find
|
|
409
408
|
* @returns {any | null} The matching entry or null
|
|
410
409
|
*/
|
|
@@ -412,30 +411,30 @@ export function findEntry(entries, id) {
|
|
|
412
411
|
return entries.find((/** @type {any} */ e) => e.id === id) ?? null;
|
|
413
412
|
}
|
|
414
413
|
|
|
415
|
-
// ───
|
|
414
|
+
// ─── Content Type Reference Resolution ──────────────────────────────────────
|
|
416
415
|
|
|
417
416
|
/**
|
|
418
|
-
* Resolve cross-
|
|
419
|
-
* "jane-doe"` with a schema `$ref` to the authors
|
|
417
|
+
* Resolve cross-content-type $ref references in entry data. For example, a blog post's `author:
|
|
418
|
+
* "jane-doe"` with a schema `$ref` to the authors content type gets resolved to the full author
|
|
420
419
|
* entry.
|
|
421
420
|
*
|
|
422
|
-
* @param {Map<string, any[]>}
|
|
421
|
+
* @param {Map<string, any[]>} contentTypes - All loaded content types @param {any} config -
|
|
423
422
|
* Content.config.json
|
|
424
423
|
*/
|
|
425
|
-
export function
|
|
426
|
-
for (const [name,
|
|
427
|
-
const cd = /** @type {any} */ (
|
|
424
|
+
export function resolveContentTypeRefs(contentTypes, config) {
|
|
425
|
+
for (const [name, contentTypeDef] of Object.entries(config.contentTypes)) {
|
|
426
|
+
const cd = /** @type {any} */ (contentTypeDef);
|
|
428
427
|
const schema = cd.schema;
|
|
429
428
|
if (!schema?.properties) continue;
|
|
430
429
|
|
|
431
|
-
const entries =
|
|
430
|
+
const entries = contentTypes.get(name);
|
|
432
431
|
if (!entries) continue;
|
|
433
432
|
|
|
434
433
|
for (const [field, def] of Object.entries(schema.properties)) {
|
|
435
434
|
const d = /** @type {any} */ (def);
|
|
436
|
-
if (!d.$ref?.startsWith("#/
|
|
437
|
-
const
|
|
438
|
-
const refEntries =
|
|
435
|
+
if (!d.$ref?.startsWith("#/contentTypes/")) continue;
|
|
436
|
+
const refContentType = d.$ref.replace("#/contentTypes/", "");
|
|
437
|
+
const refEntries = contentTypes.get(refContentType);
|
|
439
438
|
if (!refEntries) continue;
|
|
440
439
|
|
|
441
440
|
for (const entry of entries) {
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* Injects project-level and page-level context variables into a page's state before compilation.
|
|
5
5
|
* These are available as $site.* and $page.* in template expressions.
|
|
6
6
|
*
|
|
7
|
-
* Also resolves ContentCollection and ContentEntry $prototype entries against loaded content
|
|
8
|
-
*
|
|
7
|
+
* Also resolves ContentCollection and ContentEntry $prototype entries against loaded content types
|
|
8
|
+
* (Phase 2, spec §6.4).
|
|
9
9
|
*
|
|
10
10
|
* Per site-architecture spec §10: $site.name — from project.json name $site.url — from project.json
|
|
11
11
|
* url $site.state.* — site-wide reactive state $page.url — current page URL path $page.title — page
|
|
12
12
|
* title $page.params — dynamic route parameters (if any)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { queryContentType, findEntry } from "./content-loader.js";
|
|
16
16
|
import { resolve, dirname, relative } from "node:path";
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -21,7 +21,7 @@ import { resolve, dirname, relative } from "node:path";
|
|
|
21
21
|
* @param {any} doc - The page document (mutated)
|
|
22
22
|
* @param {any} projectConfig - Loaded project configuration
|
|
23
23
|
* @param {any} route - The resolved route for this page
|
|
24
|
-
* @param {Map<string, any[]>} [
|
|
24
|
+
* @param {Map<string, any[]>} [contentTypes] - Loaded content types
|
|
25
25
|
* @param {string | null} [projectRoot] - Absolute path to the project root (for import rebasing)
|
|
26
26
|
* @returns {any} The mutated document
|
|
27
27
|
*/
|
|
@@ -29,7 +29,7 @@ export function injectContext(
|
|
|
29
29
|
doc,
|
|
30
30
|
projectConfig,
|
|
31
31
|
route,
|
|
32
|
-
|
|
32
|
+
contentTypes = new Map(),
|
|
33
33
|
projectRoot = null,
|
|
34
34
|
) {
|
|
35
35
|
if (!doc.state) doc.state = {};
|
|
@@ -49,8 +49,8 @@ export function injectContext(
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
// Resolve ContentCollection and ContentEntry $prototype entries
|
|
52
|
-
if (
|
|
53
|
-
resolveContentPrototypes(doc.state,
|
|
52
|
+
if (contentTypes.size > 0) {
|
|
53
|
+
resolveContentPrototypes(doc.state, contentTypes, route._pathParams ?? {});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Merge project-level state into page state (page wins on conflicts)
|
|
@@ -110,33 +110,33 @@ export function injectContext(
|
|
|
110
110
|
/**
|
|
111
111
|
* Resolve ContentCollection and ContentEntry $prototype state entries.
|
|
112
112
|
*
|
|
113
|
-
* Replaces state entries like: { "$prototype": "ContentCollection", "
|
|
114
|
-
* with the actual resolved
|
|
113
|
+
* Replaces state entries like: { "$prototype": "ContentCollection", "contentType": "blog", ... }
|
|
114
|
+
* with the actual resolved content type data.
|
|
115
115
|
*
|
|
116
116
|
* @param {Record<string, any>} state - Page state (mutated)
|
|
117
|
-
* @param {Map<string, any[]>}
|
|
117
|
+
* @param {Map<string, any[]>} contentTypes - Loaded content types
|
|
118
118
|
* @param {Record<string, any>} params - Route parameters for $ref resolution
|
|
119
119
|
*/
|
|
120
|
-
function resolveContentPrototypes(state,
|
|
120
|
+
function resolveContentPrototypes(state, contentTypes, params) {
|
|
121
121
|
for (const [key, value] of Object.entries(state)) {
|
|
122
122
|
if (!value || typeof value !== "object" || !value.$prototype) continue;
|
|
123
123
|
|
|
124
124
|
if (value.$prototype === "ContentCollection") {
|
|
125
|
-
const entries =
|
|
125
|
+
const entries = contentTypes.get(value.contentType);
|
|
126
126
|
if (!entries) {
|
|
127
|
-
console.warn(`ContentCollection:
|
|
127
|
+
console.warn(`ContentCollection: content type "${value.contentType}" not found`);
|
|
128
128
|
state[key] = [];
|
|
129
129
|
continue;
|
|
130
130
|
}
|
|
131
|
-
state[key] =
|
|
131
|
+
state[key] = queryContentType(entries, {
|
|
132
132
|
filter: value.filter,
|
|
133
133
|
sort: value.sort,
|
|
134
134
|
limit: value.limit,
|
|
135
135
|
});
|
|
136
136
|
} else if (value.$prototype === "ContentEntry") {
|
|
137
|
-
const entries =
|
|
137
|
+
const entries = contentTypes.get(value.contentType);
|
|
138
138
|
if (!entries) {
|
|
139
|
-
console.warn(`ContentEntry:
|
|
139
|
+
console.warn(`ContentEntry: content type "${value.contentType}" not found`);
|
|
140
140
|
state[key] = null;
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
@@ -162,17 +162,17 @@ function fileToRoute(relativePath, absolutePath) {
|
|
|
162
162
|
/**
|
|
163
163
|
* Expand dynamic routes by resolving $paths from each dynamic page.
|
|
164
164
|
*
|
|
165
|
-
* Supports three $paths shapes (per spec §4.3): 1.
|
|
166
|
-
* "slug", field: "id" } 2. Explicit values: { values: ["en", "fr"], param: "lang" } 3. Data
|
|
167
|
-
* ref: { "$ref": "./data/products.json", param: "id", field: "sku" } 4. Legacy array: [{ slug:
|
|
165
|
+
* Supports three $paths shapes (per spec §4.3): 1. Content type-based: { contentType: "blog",
|
|
166
|
+
* param: "slug", field: "id" } 2. Explicit values: { values: ["en", "fr"], param: "lang" } 3. Data
|
|
167
|
+
* file ref: { "$ref": "./data/products.json", param: "id", field: "sku" } 4. Legacy array: [{ slug:
|
|
168
168
|
* "hello" }, { slug: "world" }]
|
|
169
169
|
*
|
|
170
170
|
* @param {Route[]} routes - Discovered route table
|
|
171
171
|
* @param {string} projectRoot - Project root for resolving $ref paths
|
|
172
|
-
* @param {Map<string, any[]>} [
|
|
172
|
+
* @param {Map<string, any[]>} [contentTypes] - Loaded content types (from content-loader)
|
|
173
173
|
* @returns {Promise<Route[]>} Expanded routes with concrete paths
|
|
174
174
|
*/
|
|
175
|
-
export async function expandDynamicRoutes(routes, projectRoot,
|
|
175
|
+
export async function expandDynamicRoutes(routes, projectRoot, contentTypes = new Map()) {
|
|
176
176
|
/** @type {Route[]} */
|
|
177
177
|
const expanded = [];
|
|
178
178
|
|
|
@@ -197,7 +197,7 @@ export async function expandDynamicRoutes(routes, projectRoot, collections = new
|
|
|
197
197
|
continue;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
const pathEntries = resolvePathEntries(raw.$paths, projectRoot,
|
|
200
|
+
const pathEntries = resolvePathEntries(raw.$paths, projectRoot, contentTypes);
|
|
201
201
|
|
|
202
202
|
for (const pathEntry of pathEntries) {
|
|
203
203
|
let concreteUrl = route.urlPattern;
|
|
@@ -225,21 +225,21 @@ export async function expandDynamicRoutes(routes, projectRoot, collections = new
|
|
|
225
225
|
*
|
|
226
226
|
* @param {any} $paths - The $paths declaration
|
|
227
227
|
* @param {string} projectRoot
|
|
228
|
-
* @param {Map<string, any[]>}
|
|
228
|
+
* @param {Map<string, any[]>} contentTypes
|
|
229
229
|
* @returns {Record<string, any>[]} Array of { paramName: value } objects
|
|
230
230
|
*/
|
|
231
|
-
function resolvePathEntries($paths, projectRoot,
|
|
231
|
+
function resolvePathEntries($paths, projectRoot, contentTypes) {
|
|
232
232
|
// Legacy: array of param objects
|
|
233
233
|
if (Array.isArray($paths)) {
|
|
234
234
|
return $paths;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
//
|
|
238
|
-
if ($paths.
|
|
239
|
-
const entries =
|
|
237
|
+
// Content type-based: { contentType: "blog", param: "slug", field: "id" }
|
|
238
|
+
if ($paths.contentType) {
|
|
239
|
+
const entries = contentTypes.get($paths.contentType);
|
|
240
240
|
if (!entries || entries.length === 0) {
|
|
241
241
|
console.warn(
|
|
242
|
-
`Warning: $paths references
|
|
242
|
+
`Warning: $paths references content type "${$paths.contentType}" but it has no entries`,
|
|
243
243
|
);
|
|
244
244
|
return [];
|
|
245
245
|
}
|