@jxsuite/compiler 0.10.1 → 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/package.json +3 -3
- package/src/site/content-loader.js +56 -56
- package/src/site/context-injection.js +16 -16
- package/src/site/pages-discovery.js +12 -12
- package/src/site/site-build.js +13 -13
- package/src/site/site-loader.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.10.
|
|
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.10.
|
|
41
|
-
"@jxsuite/runtime": "^0.10.
|
|
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",
|
|
@@ -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
|
*
|
|
@@ -99,7 +99,7 @@ let _mdModule = null;
|
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Lazily import @jxsuite/parser for Markdown support. This avoids hard dependency — only loads when
|
|
102
|
-
* MD
|
|
102
|
+
* MD content types exist.
|
|
103
103
|
*
|
|
104
104
|
* @returns {Promise<any>}
|
|
105
105
|
*/
|
|
@@ -172,7 +172,7 @@ function loadJSONEntries(filePath) {
|
|
|
172
172
|
* Load a CSV file into ContentEntry(s).
|
|
173
173
|
*
|
|
174
174
|
* @param {string} filePath - Absolute path to .csv file
|
|
175
|
-
* @param {any} [schema] -
|
|
175
|
+
* @param {any} [schema] - Content type schema (for type coercion)
|
|
176
176
|
* @returns {object[]} Array of ContentEntry shapes
|
|
177
177
|
*/
|
|
178
178
|
function loadCSVEntries(filePath, schema) {
|
|
@@ -198,10 +198,10 @@ function loadCSVEntries(filePath, schema) {
|
|
|
198
198
|
// ─── Content Config ───────────────────────────────────────────────────────────
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
|
-
* Load and parse content
|
|
201
|
+
* Load and parse content types config from project.json.
|
|
202
202
|
*
|
|
203
203
|
* @param {string} projectRoot - Project root directory
|
|
204
|
-
* @param {Record<string, any>} [projectConfig] - Already-loaded project config with `
|
|
204
|
+
* @param {Record<string, any>} [projectConfig] - Already-loaded project config with `contentTypes`
|
|
205
205
|
* key
|
|
206
206
|
* @returns {{ config: any; contentDir: string } | null} Parsed config or null if no content dir
|
|
207
207
|
*/
|
|
@@ -209,68 +209,68 @@ export function loadContentConfig(projectRoot, projectConfig = undefined) {
|
|
|
209
209
|
const contentDir = resolve(projectRoot, "content");
|
|
210
210
|
|
|
211
211
|
/** @type {any} */
|
|
212
|
-
const config = {
|
|
212
|
+
const config = { contentTypes: projectConfig?.contentTypes ?? {} };
|
|
213
213
|
|
|
214
214
|
return { config, contentDir };
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// ───
|
|
217
|
+
// ─── Content Type Loading ────────────────────────────────────────────────────
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
* Load all content
|
|
220
|
+
* Load all content types defined in project.json.
|
|
221
221
|
*
|
|
222
222
|
* @param {string} projectRoot - Project root directory
|
|
223
223
|
* @param {Record<string, any>} [projectConfig] - Already-loaded project config
|
|
224
|
-
* @returns {Promise<Map<string, any[]>>} Map of
|
|
224
|
+
* @returns {Promise<Map<string, any[]>>} Map of content type name → array of ContentEntry
|
|
225
225
|
*/
|
|
226
|
-
export async function
|
|
226
|
+
export async function loadContentTypes(projectRoot, projectConfig = undefined) {
|
|
227
227
|
const result = loadContentConfig(projectRoot, projectConfig);
|
|
228
228
|
if (!result) return new Map();
|
|
229
229
|
|
|
230
230
|
const { config } = result;
|
|
231
231
|
/** @type {Map<string, any[]>} */
|
|
232
|
-
const
|
|
232
|
+
const contentTypes = new Map();
|
|
233
233
|
|
|
234
|
-
for (const [name,
|
|
235
|
-
const entries = await
|
|
236
|
-
|
|
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);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
return
|
|
239
|
+
return contentTypes;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
|
-
* Get the $elements array for a specific
|
|
243
|
+
* Get the $elements array for a specific content type, if defined in project.json contentTypes.
|
|
244
244
|
*
|
|
245
245
|
* @param {string} projectRoot - Project root directory
|
|
246
|
-
* @param {string}
|
|
246
|
+
* @param {string} contentTypeName - Name of the content type
|
|
247
247
|
* @param {Record<string, any>} [projectConfig] - Already-loaded project config
|
|
248
248
|
* @returns {any[] | undefined}
|
|
249
249
|
*/
|
|
250
|
-
export function
|
|
250
|
+
export function getContentTypeElements(projectRoot, contentTypeName, projectConfig = undefined) {
|
|
251
251
|
const result = loadContentConfig(projectRoot, projectConfig);
|
|
252
252
|
if (!result) return undefined;
|
|
253
|
-
const def = result.config.
|
|
253
|
+
const def = result.config.contentTypes?.[contentTypeName];
|
|
254
254
|
return def?.$elements;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* Load a single
|
|
258
|
+
* Load a single content type by its definition.
|
|
259
259
|
*
|
|
260
|
-
* @param {string} name -
|
|
261
|
-
* @param {any}
|
|
260
|
+
* @param {string} name - Content type name
|
|
261
|
+
* @param {any} contentTypeDef - Content type definition from project.json
|
|
262
262
|
* @param {string} projectRoot - Absolute path to project root directory
|
|
263
263
|
* @returns {Promise<any[]>} Array of ContentEntry
|
|
264
264
|
*/
|
|
265
|
-
async function
|
|
266
|
-
const source =
|
|
267
|
-
const schema =
|
|
265
|
+
async function loadContentType(name, contentTypeDef, projectRoot) {
|
|
266
|
+
const source = contentTypeDef.source;
|
|
267
|
+
const schema = contentTypeDef.schema;
|
|
268
268
|
|
|
269
|
-
// Derive directive allowedNames from
|
|
269
|
+
// Derive directive allowedNames from content type $elements (tag names from npm packages)
|
|
270
270
|
/** @type {any} */
|
|
271
|
-
const directiveOptions =
|
|
271
|
+
const directiveOptions = contentTypeDef.$elements?.length
|
|
272
272
|
? {
|
|
273
|
-
allowedNames:
|
|
273
|
+
allowedNames: contentTypeDef.$elements
|
|
274
274
|
.filter((/** @type {any} */ e) => typeof e === "string" || e?.$ref)
|
|
275
275
|
.map((/** @type {any} */ e) => (typeof e === "string" ? e : e.$ref)),
|
|
276
276
|
}
|
|
@@ -306,14 +306,14 @@ async function loadCollection(name, collectionDef, projectRoot) {
|
|
|
306
306
|
// ─── Schema Validation ────────────────────────────────────────────────────────
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
|
-
* Validate content entries against their
|
|
309
|
+
* Validate content entries against their content type schema. Logs warnings for missing required
|
|
310
310
|
* fields and type mismatches.
|
|
311
311
|
*
|
|
312
312
|
* @param {any[]} entries - Array of ContentEntry
|
|
313
|
-
* @param {any} schema - JSON Schema for the
|
|
314
|
-
* @param {string}
|
|
313
|
+
* @param {any} schema - JSON Schema for the content type
|
|
314
|
+
* @param {string} contentTypeName - For error messages
|
|
315
315
|
*/
|
|
316
|
-
function validateEntries(entries, schema,
|
|
316
|
+
function validateEntries(entries, schema, contentTypeName) {
|
|
317
317
|
const required = schema.required ?? [];
|
|
318
318
|
const properties = schema.properties ?? {};
|
|
319
319
|
|
|
@@ -322,7 +322,7 @@ function validateEntries(entries, schema, collectionName) {
|
|
|
322
322
|
for (const field of required) {
|
|
323
323
|
if (!(field in entry.data) || entry.data[field] == null) {
|
|
324
324
|
console.warn(
|
|
325
|
-
`Content validation: "${
|
|
325
|
+
`Content validation: "${contentTypeName}/${entry.id}" missing required field "${field}"`,
|
|
326
326
|
);
|
|
327
327
|
}
|
|
328
328
|
}
|
|
@@ -335,36 +335,36 @@ function validateEntries(entries, schema, collectionName) {
|
|
|
335
335
|
|
|
336
336
|
if (d.type === "string" && typeof value !== "string") {
|
|
337
337
|
console.warn(
|
|
338
|
-
`Content validation: "${
|
|
338
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected string, got ${typeof value}`,
|
|
339
339
|
);
|
|
340
340
|
} else if (d.type === "number" && typeof value !== "number") {
|
|
341
341
|
console.warn(
|
|
342
|
-
`Content validation: "${
|
|
342
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected number, got ${typeof value}`,
|
|
343
343
|
);
|
|
344
344
|
} else if (d.type === "boolean" && typeof value !== "boolean") {
|
|
345
345
|
console.warn(
|
|
346
|
-
`Content validation: "${
|
|
346
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected boolean, got ${typeof value}`,
|
|
347
347
|
);
|
|
348
348
|
} else if (d.type === "array" && !Array.isArray(value)) {
|
|
349
349
|
console.warn(
|
|
350
|
-
`Content validation: "${
|
|
350
|
+
`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected array, got ${typeof value}`,
|
|
351
351
|
);
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
// ───
|
|
357
|
+
// ─── Content Type Querying ───────────────────────────────────────────────────
|
|
358
358
|
|
|
359
359
|
/**
|
|
360
|
-
* Query a loaded
|
|
360
|
+
* Query a loaded content type with filter, sort, and limit. Implements the ContentCollection
|
|
361
361
|
* $prototype resolution.
|
|
362
362
|
*
|
|
363
|
-
* @param {any[]} entries - Full
|
|
363
|
+
* @param {any[]} entries - Full content type entries
|
|
364
364
|
* @param {any} [query] - Query options
|
|
365
365
|
* @returns {any[]} Filtered, sorted, limited entries
|
|
366
366
|
*/
|
|
367
|
-
export function
|
|
367
|
+
export function queryContentType(entries, query = {}) {
|
|
368
368
|
let result = [...entries];
|
|
369
369
|
|
|
370
370
|
// Filter
|
|
@@ -401,9 +401,9 @@ export function queryCollection(entries, query = {}) {
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
/**
|
|
404
|
-
* Find a single entry by ID in a
|
|
404
|
+
* Find a single entry by ID in a content type. Implements the ContentEntry $prototype resolution.
|
|
405
405
|
*
|
|
406
|
-
* @param {any[]} entries - Full
|
|
406
|
+
* @param {any[]} entries - Full content type entries
|
|
407
407
|
* @param {string} id - Entry ID to find
|
|
408
408
|
* @returns {any | null} The matching entry or null
|
|
409
409
|
*/
|
|
@@ -411,30 +411,30 @@ export function findEntry(entries, id) {
|
|
|
411
411
|
return entries.find((/** @type {any} */ e) => e.id === id) ?? null;
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
// ───
|
|
414
|
+
// ─── Content Type Reference Resolution ──────────────────────────────────────
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
|
-
* Resolve cross-
|
|
418
|
-
* "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
|
|
419
419
|
* entry.
|
|
420
420
|
*
|
|
421
|
-
* @param {Map<string, any[]>}
|
|
421
|
+
* @param {Map<string, any[]>} contentTypes - All loaded content types @param {any} config -
|
|
422
422
|
* Content.config.json
|
|
423
423
|
*/
|
|
424
|
-
export function
|
|
425
|
-
for (const [name,
|
|
426
|
-
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);
|
|
427
427
|
const schema = cd.schema;
|
|
428
428
|
if (!schema?.properties) continue;
|
|
429
429
|
|
|
430
|
-
const entries =
|
|
430
|
+
const entries = contentTypes.get(name);
|
|
431
431
|
if (!entries) continue;
|
|
432
432
|
|
|
433
433
|
for (const [field, def] of Object.entries(schema.properties)) {
|
|
434
434
|
const d = /** @type {any} */ (def);
|
|
435
|
-
if (!d.$ref?.startsWith("#/
|
|
436
|
-
const
|
|
437
|
-
const refEntries =
|
|
435
|
+
if (!d.$ref?.startsWith("#/contentTypes/")) continue;
|
|
436
|
+
const refContentType = d.$ref.replace("#/contentTypes/", "");
|
|
437
|
+
const refEntries = contentTypes.get(refContentType);
|
|
438
438
|
if (!refEntries) continue;
|
|
439
439
|
|
|
440
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
|
}
|
package/src/site/site-build.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
DEFAULT_REACTIVITY_SRC,
|
|
41
41
|
DEFAULT_LIT_HTML_SRC,
|
|
42
42
|
} from "../shared.js";
|
|
43
|
-
import {
|
|
43
|
+
import { loadContentTypes, loadContentConfig, resolveContentTypeRefs } from "./content-loader.js";
|
|
44
44
|
import { resolvePrototypes } from "./prototype-resolver.js";
|
|
45
45
|
import { compileMarkdown } from "../targets/compile-markdown.js";
|
|
46
46
|
import { transformImageNodes } from "./image-transform.js";
|
|
@@ -97,20 +97,20 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
97
97
|
const staticRoutes = discoverPages(pagesDir);
|
|
98
98
|
log(` Found ${staticRoutes.length} page(s)`);
|
|
99
99
|
|
|
100
|
-
// ── 3b. Load content
|
|
101
|
-
log("Loading content
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
log(` Loaded ${
|
|
105
|
-
// Resolve cross-
|
|
100
|
+
// ── 3b. Load content types ─────────────────────────────────────────────
|
|
101
|
+
log("Loading content types...");
|
|
102
|
+
const contentTypes = await loadContentTypes(projectRoot, projectConfig);
|
|
103
|
+
if (contentTypes.size > 0) {
|
|
104
|
+
log(` Loaded ${contentTypes.size} content type(s): ${[...contentTypes.keys()].join(", ")}`);
|
|
105
|
+
// Resolve cross-content-type $ref references
|
|
106
106
|
const contentConfig = loadContentConfig(projectRoot, projectConfig);
|
|
107
107
|
if (contentConfig) {
|
|
108
|
-
|
|
108
|
+
resolveContentTypeRefs(contentTypes, contentConfig.config);
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// ── 4. Expand dynamic routes ────────────────────────────────────────────
|
|
113
|
-
const routes = await expandDynamicRoutes(staticRoutes, projectRoot,
|
|
113
|
+
const routes = await expandDynamicRoutes(staticRoutes, projectRoot, contentTypes);
|
|
114
114
|
log(` ${routes.length} route(s) after expansion`);
|
|
115
115
|
|
|
116
116
|
let fileCount = 0;
|
|
@@ -194,7 +194,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
194
194
|
route,
|
|
195
195
|
projectConfig,
|
|
196
196
|
projectRoot,
|
|
197
|
-
|
|
197
|
+
contentTypes,
|
|
198
198
|
imageCache,
|
|
199
199
|
outDir,
|
|
200
200
|
componentDefs,
|
|
@@ -339,7 +339,7 @@ export async function buildSite(projectRoot, options = {}) {
|
|
|
339
339
|
* @param {any} route
|
|
340
340
|
* @param {any} projectConfig
|
|
341
341
|
* @param {string} projectRoot
|
|
342
|
-
* @param {Map<string, any[]>} [
|
|
342
|
+
* @param {Map<string, any[]>} [contentTypes]
|
|
343
343
|
* @param {import("./image-cache.js").CacheManifest | null} [imageCache]
|
|
344
344
|
* @param {string} [outDir]
|
|
345
345
|
* @returns {Promise<{ html: string; files: any[]; serverHandler: string | null; doc: any }>}
|
|
@@ -348,7 +348,7 @@ async function compilePage(
|
|
|
348
348
|
route,
|
|
349
349
|
projectConfig,
|
|
350
350
|
projectRoot,
|
|
351
|
-
|
|
351
|
+
contentTypes = new Map(),
|
|
352
352
|
imageCache = null,
|
|
353
353
|
outDir = "",
|
|
354
354
|
componentDefs = new Map(),
|
|
@@ -375,7 +375,7 @@ async function compilePage(
|
|
|
375
375
|
delete layoutDoc._pageTitle;
|
|
376
376
|
|
|
377
377
|
// Inject $site and $page context, resolve ContentCollection/ContentEntry
|
|
378
|
-
injectContext(layoutDoc, projectConfig, route,
|
|
378
|
+
injectContext(layoutDoc, projectConfig, route, contentTypes, projectRoot);
|
|
379
379
|
|
|
380
380
|
// Resolve generic $prototype entries via .class.json imports
|
|
381
381
|
await resolvePrototypes(layoutDoc, route, projectRoot);
|
package/src/site/site-loader.js
CHANGED
|
@@ -25,7 +25,7 @@ const DEFAULTS = {
|
|
|
25
25
|
$media: {},
|
|
26
26
|
style: {},
|
|
27
27
|
state: {},
|
|
28
|
-
|
|
28
|
+
contentTypes: {},
|
|
29
29
|
redirects: {},
|
|
30
30
|
images: {
|
|
31
31
|
optimize: true,
|
|
@@ -85,7 +85,7 @@ export function loadProjectConfig(projectRoot) {
|
|
|
85
85
|
if (raw.state) config.state = raw.state;
|
|
86
86
|
if (raw.redirects) config.redirects = raw.redirects;
|
|
87
87
|
if (raw.imports) config.imports = raw.imports;
|
|
88
|
-
if (raw.
|
|
88
|
+
if (raw.contentTypes) config.contentTypes = raw.contentTypes;
|
|
89
89
|
|
|
90
90
|
return {
|
|
91
91
|
config,
|