@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.9.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.7.0",
41
- "@jxsuite/runtime": "^0.7.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",
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./compiler.js";
3
+
4
+ const [, , src, out] = process.argv;
5
+ if (src) {
6
+ runCli(src, out).catch((err) => {
7
+ console.error(err);
8
+ process.exit(1);
9
+ });
10
+ }
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/compiler.js <source.json> [output.html]
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
- const isMainModule = process.argv[1]?.endsWith("compiler.js");
136
- if (isMainModule && process.argv[2]) {
137
- const [, , src, out] = process.argv;
138
-
139
- Promise.all([compile(src), compileServer(src)])
140
- .then(async ([result, server]) => {
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
- }
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 collection loader
2
+ * Content-loader.js — Content type loader
3
3
  *
4
- * Loads content collections defined in project.json's `collections` key. Supports Markdown (.md),
5
- * JSON (.json), and CSV (.csv) source files.
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 collections exist.
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] - Collection schema (for type coercion)
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, rendered: 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 collections config from project.json.
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 `collections`
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 = { collections: projectConfig?.collections ?? {} };
212
+ const config = { contentTypes: projectConfig?.contentTypes ?? {} };
214
213
 
215
214
  return { config, contentDir };
216
215
  }
217
216
 
218
- // ─── Collection Loading ───────────────────────────────────────────────────────
217
+ // ─── Content Type Loading ────────────────────────────────────────────────────
219
218
 
220
219
  /**
221
- * Load all content collections defined in project.json.
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 collection name → array of ContentEntry
224
+ * @returns {Promise<Map<string, any[]>>} Map of content type name → array of ContentEntry
226
225
  */
227
- export async function loadCollections(projectRoot, projectConfig = undefined) {
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 collections = new Map();
232
+ const contentTypes = new Map();
234
233
 
235
- for (const [name, collectionDef] of Object.entries(config.collections)) {
236
- const entries = await loadCollection(name, /** @type {any} */ (collectionDef), projectRoot);
237
- collections.set(name, entries);
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 collections;
239
+ return contentTypes;
241
240
  }
242
241
 
243
242
  /**
244
- * Get the $elements array for a specific collection, if defined in project.json collections.
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} collectionName - Name of the collection
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 getCollectionElements(projectRoot, collectionName, projectConfig = undefined) {
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.collections?.[collectionName];
253
+ const def = result.config.contentTypes?.[contentTypeName];
255
254
  return def?.$elements;
256
255
  }
257
256
 
258
257
  /**
259
- * Load a single collection by its definition.
258
+ * Load a single content type by its definition.
260
259
  *
261
- * @param {string} name - Collection name
262
- * @param {any} collectionDef - Collection definition from project.json
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 loadCollection(name, collectionDef, projectRoot) {
267
- const source = collectionDef.source;
268
- const schema = collectionDef.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 collection $elements (tag names from npm packages)
269
+ // Derive directive allowedNames from content type $elements (tag names from npm packages)
271
270
  /** @type {any} */
272
- const directiveOptions = collectionDef.$elements?.length
271
+ const directiveOptions = contentTypeDef.$elements?.length
273
272
  ? {
274
- allowedNames: collectionDef.$elements
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 collection schema. Logs warnings for missing required
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 collection
315
- * @param {string} collectionName - For error messages
313
+ * @param {any} schema - JSON Schema for the content type
314
+ * @param {string} contentTypeName - For error messages
316
315
  */
317
- function validateEntries(entries, schema, collectionName) {
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: "${collectionName}/${entry.id}" missing required field "${field}"`,
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: "${collectionName}/${entry.id}" field "${field}" expected string, got ${typeof value}`,
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: "${collectionName}/${entry.id}" field "${field}" expected number, got ${typeof value}`,
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: "${collectionName}/${entry.id}" field "${field}" expected boolean, got ${typeof value}`,
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: "${collectionName}/${entry.id}" field "${field}" expected array, got ${typeof value}`,
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
- // ─── Collection Querying ──────────────────────────────────────────────────────
357
+ // ─── Content Type Querying ───────────────────────────────────────────────────
359
358
 
360
359
  /**
361
- * Query a loaded collection with filter, sort, and limit. Implements the ContentCollection
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 collection entries
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 queryCollection(entries, query = {}) {
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 collection. Implements the ContentEntry $prototype resolution.
404
+ * Find a single entry by ID in a content type. Implements the ContentEntry $prototype resolution.
406
405
  *
407
- * @param {any[]} entries - Full collection entries
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
- // ─── Collection Reference Resolution ─────────────────────────────────────────
414
+ // ─── Content Type Reference Resolution ──────────────────────────────────────
416
415
 
417
416
  /**
418
- * Resolve cross-collection $ref references in entry data. For example, a blog post's `author:
419
- * "jane-doe"` with a schema `$ref` to the authors collection gets resolved to the full author
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[]>} collections - All loaded collections @param {any} config -
421
+ * @param {Map<string, any[]>} contentTypes - All loaded content types @param {any} config -
423
422
  * Content.config.json
424
423
  */
425
- export function resolveCollectionRefs(collections, config) {
426
- for (const [name, collectionDef] of Object.entries(config.collections)) {
427
- const cd = /** @type {any} */ (collectionDef);
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 = collections.get(name);
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("#/collections/")) continue;
437
- const refCollection = d.$ref.replace("#/collections/", "");
438
- const refEntries = collections.get(refCollection);
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
- * collections (Phase 2, spec §6.4).
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 { queryCollection, findEntry } from "./content-loader.js";
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[]>} [collections] - Loaded content collections
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
- collections = new Map(),
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 (collections.size > 0) {
53
- resolveContentPrototypes(doc.state, collections, route._pathParams ?? {});
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", "collection": "blog", ... }
114
- * with the actual resolved collection data.
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[]>} collections - Loaded collections
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, collections, params) {
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 = collections.get(value.collection);
125
+ const entries = contentTypes.get(value.contentType);
126
126
  if (!entries) {
127
- console.warn(`ContentCollection: collection "${value.collection}" not found`);
127
+ console.warn(`ContentCollection: content type "${value.contentType}" not found`);
128
128
  state[key] = [];
129
129
  continue;
130
130
  }
131
- state[key] = queryCollection(entries, {
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 = collections.get(value.collection);
137
+ const entries = contentTypes.get(value.contentType);
138
138
  if (!entries) {
139
- console.warn(`ContentEntry: collection "${value.collection}" not found`);
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. Collection-based: { collection: "blog", param:
166
- * "slug", field: "id" } 2. Explicit values: { values: ["en", "fr"], param: "lang" } 3. Data file
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[]>} [collections] - Loaded content collections (from content-loader)
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, collections = new Map()) {
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, collections);
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[]>} collections
228
+ * @param {Map<string, any[]>} contentTypes
229
229
  * @returns {Record<string, any>[]} Array of { paramName: value } objects
230
230
  */
231
- function resolvePathEntries($paths, projectRoot, collections) {
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
- // Collection-based: { collection: "blog", param: "slug", field: "id" }
238
- if ($paths.collection) {
239
- const entries = collections.get($paths.collection);
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 collection "${$paths.collection}" but it has no entries`,
242
+ `Warning: $paths references content type "${$paths.contentType}" but it has no entries`,
243
243
  );
244
244
  return [];
245
245
  }