@jxsuite/compiler 0.21.4 → 0.22.0
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.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@apidevtools/json-schema-ref-parser": "^15.3.5",
|
|
41
|
-
"@jxsuite/parser": "^0.
|
|
42
|
-
"@jxsuite/runtime": "^0.
|
|
41
|
+
"@jxsuite/parser": "^0.22.0",
|
|
42
|
+
"@jxsuite/runtime": "^0.22.0",
|
|
43
43
|
"remark-gfm": "^4.0.1",
|
|
44
44
|
"remark-stringify": "^11.0.0",
|
|
45
45
|
"sharp": "^0.34.5",
|
|
@@ -381,67 +381,9 @@ function validateEntries(entries, schema, contentTypeName) {
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// ─── Content Type Querying
|
|
384
|
+
// ─── Content Type Querying (re-exported from @jxsuite/parser/content) ────────
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
* Query a loaded content type with filter, sort, and limit. Implements the ContentCollection
|
|
388
|
-
* $prototype resolution.
|
|
389
|
-
*
|
|
390
|
-
* @param {ContentLoaderEntry[]} entries - Full content type entries
|
|
391
|
-
* @param {{
|
|
392
|
-
* filter?: Record<string, unknown>;
|
|
393
|
-
* sort?: { field: string; order?: string };
|
|
394
|
-
* limit?: number;
|
|
395
|
-
* }} [query]
|
|
396
|
-
* - Query options
|
|
397
|
-
* @returns {ContentLoaderEntry[]} Filtered, sorted, limited entries
|
|
398
|
-
*/
|
|
399
|
-
export function queryContentType(entries, query = {}) {
|
|
400
|
-
let result = [...entries];
|
|
401
|
-
|
|
402
|
-
// Filter
|
|
403
|
-
if (query.filter && typeof query.filter === "object") {
|
|
404
|
-
result = result.filter((entry) => {
|
|
405
|
-
for (const [key, expected] of Object.entries(
|
|
406
|
-
/** @type {Record<string, unknown>} */ (query.filter),
|
|
407
|
-
)) {
|
|
408
|
-
const actual = entry.data[key];
|
|
409
|
-
if (actual !== expected) return false;
|
|
410
|
-
}
|
|
411
|
-
return true;
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Sort
|
|
416
|
-
if (query.sort) {
|
|
417
|
-
const { field, order = "asc" } = query.sort;
|
|
418
|
-
result.sort((a, b) => {
|
|
419
|
-
const aVal = /** @type {string | number} */ (a.data[field] ?? "");
|
|
420
|
-
const bVal = /** @type {string | number} */ (b.data[field] ?? "");
|
|
421
|
-
if (aVal < bVal) return order === "asc" ? -1 : 1;
|
|
422
|
-
if (aVal > bVal) return order === "asc" ? 1 : -1;
|
|
423
|
-
return 0;
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Limit
|
|
428
|
-
if (query.limit && query.limit > 0) {
|
|
429
|
-
result = result.slice(0, query.limit);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return result;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Find a single entry by ID in a content type. Implements the ContentEntry $prototype resolution.
|
|
437
|
-
*
|
|
438
|
-
* @param {ContentLoaderEntry[]} entries - Full content type entries
|
|
439
|
-
* @param {string} id - Entry ID to find
|
|
440
|
-
* @returns {ContentLoaderEntry | null} The matching entry or null
|
|
441
|
-
*/
|
|
442
|
-
export function findEntry(entries, id) {
|
|
443
|
-
return entries.find((e) => e.id === id) ?? null;
|
|
444
|
-
}
|
|
386
|
+
export { queryContentType, findEntry } from "@jxsuite/parser/content";
|
|
445
387
|
|
|
446
388
|
// ─── Content Type Reference Resolution ──────────────────────────────────────
|
|
447
389
|
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* title $page.params — dynamic route parameters (if any)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { queryContentType, findEntry } from "./content-loader.js";
|
|
16
15
|
import { resolve, dirname, relative } from "node:path";
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -48,11 +47,6 @@ export function injectContext(
|
|
|
48
47
|
params: route._pathParams ?? {},
|
|
49
48
|
};
|
|
50
49
|
|
|
51
|
-
// Resolve ContentCollection and ContentEntry $prototype entries
|
|
52
|
-
if (contentTypes.size > 0) {
|
|
53
|
-
resolveContentPrototypes(doc.state, contentTypes, route._pathParams ?? {});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
50
|
// Merge project-level state into page state (page wins on conflicts)
|
|
57
51
|
if (projectConfig.state) {
|
|
58
52
|
for (const [key, value] of Object.entries(projectConfig.state)) {
|
|
@@ -110,49 +104,3 @@ export function injectContext(
|
|
|
110
104
|
|
|
111
105
|
return doc;
|
|
112
106
|
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Resolve ContentCollection and ContentEntry $prototype state entries.
|
|
116
|
-
*
|
|
117
|
-
* Replaces state entries like: { "$prototype": "ContentCollection", "contentType": "blog", ... }
|
|
118
|
-
* with the actual resolved content type data.
|
|
119
|
-
*
|
|
120
|
-
* @param {Record<string, unknown>} state - Page state (mutated)
|
|
121
|
-
* @param {Map<string, ContentLoaderEntry[]>} contentTypes - Loaded content types
|
|
122
|
-
* @param {Record<string, string>} params - Route parameters for $ref resolution
|
|
123
|
-
*/
|
|
124
|
-
function resolveContentPrototypes(state, contentTypes, params) {
|
|
125
|
-
for (const [key, value] of Object.entries(state)) {
|
|
126
|
-
if (!value || typeof value !== "object" || !(/** @type {JxMutableNode} */ (value).$prototype))
|
|
127
|
-
continue;
|
|
128
|
-
const v = /** @type {JxPrototypeDef} */ (value);
|
|
129
|
-
|
|
130
|
-
if (v.$prototype === "ContentCollection") {
|
|
131
|
-
const entries = contentTypes.get(/** @type {string} */ (v.contentType));
|
|
132
|
-
if (!entries) {
|
|
133
|
-
console.warn(`ContentCollection: content type "${v.contentType}" not found`);
|
|
134
|
-
state[key] = [];
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
state[key] = queryContentType(entries, {
|
|
138
|
-
filter: v.filter,
|
|
139
|
-
sort: v.sort,
|
|
140
|
-
limit: v.limit,
|
|
141
|
-
});
|
|
142
|
-
} else if (v.$prototype === "ContentEntry") {
|
|
143
|
-
const entries = contentTypes.get(/** @type {string} */ (v.contentType));
|
|
144
|
-
if (!entries) {
|
|
145
|
-
console.warn(`ContentEntry: content type "${v.contentType}" not found`);
|
|
146
|
-
state[key] = null;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
// Resolve the ID — may reference $params
|
|
150
|
-
let id = v.id;
|
|
151
|
-
if (id && typeof id === "object" && id.$ref?.startsWith("#/$params/")) {
|
|
152
|
-
const paramName = id.$ref.replace("#/$params/", "");
|
|
153
|
-
id = params[paramName];
|
|
154
|
-
}
|
|
155
|
-
state[key] = findEntry(entries, /** @type {string} */ (id));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
package/src/site/head-merger.js
CHANGED
|
@@ -94,7 +94,9 @@ function headEntryKey(entry) {
|
|
|
94
94
|
|
|
95
95
|
// <style> — unique per content hash
|
|
96
96
|
if (tag === "style") {
|
|
97
|
-
const content = Array.isArray(entry.children)
|
|
97
|
+
const content = Array.isArray(entry.children)
|
|
98
|
+
? entry.children.join("")
|
|
99
|
+
: (entry.textContent ?? "");
|
|
98
100
|
return `style:${simpleHash(content)}`;
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -149,7 +151,9 @@ function renderHeadEntry(entry) {
|
|
|
149
151
|
if (VOID.has(tag)) return open;
|
|
150
152
|
|
|
151
153
|
// Elements with content
|
|
152
|
-
const content = Array.isArray(entry.children)
|
|
154
|
+
const content = Array.isArray(entry.children)
|
|
155
|
+
? entry.children.join("")
|
|
156
|
+
: (entry.textContent ?? "");
|
|
153
157
|
return `${open}${content}</${tag}>`;
|
|
154
158
|
}
|
|
155
159
|
|
|
@@ -23,24 +23,7 @@ import { createRequire } from "node:module";
|
|
|
23
23
|
* Prototype names handled elsewhere (builtins + legacy content system). These are skipped by the
|
|
24
24
|
* generic resolver.
|
|
25
25
|
*/
|
|
26
|
-
const SKIP_PROTOTYPES = new Set([
|
|
27
|
-
"Function",
|
|
28
|
-
"LocalStorage",
|
|
29
|
-
"SessionStorage",
|
|
30
|
-
"Array",
|
|
31
|
-
"ContentCollection",
|
|
32
|
-
"ContentEntry",
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Built-in $prototype → .class.json mappings. These resolve automatically without requiring an
|
|
37
|
-
* explicit `imports` entry in project.json. Project-level imports take precedence on collision.
|
|
38
|
-
*/
|
|
39
|
-
/** @type {Record<string, string>} */
|
|
40
|
-
const BUILTIN_CLASS_MAPPINGS = {
|
|
41
|
-
MarkdownFile: "@jxsuite/parser/MarkdownFile.class.json",
|
|
42
|
-
MarkdownCollection: "@jxsuite/parser/MarkdownCollection.class.json",
|
|
43
|
-
};
|
|
26
|
+
const SKIP_PROTOTYPES = new Set(["Function", "LocalStorage", "SessionStorage", "Array"]);
|
|
44
27
|
|
|
45
28
|
/**
|
|
46
29
|
* Keys reserved by the Jx prototype system — stripped before passing config to the external class
|
|
@@ -66,10 +49,12 @@ const RESERVED_KEYS = new Set([
|
|
|
66
49
|
* result
|
|
67
50
|
*
|
|
68
51
|
* @param {JxMutableNode | JxDocument} doc - The page document (mutated in place)
|
|
69
|
-
* @param {{ sourcePath?: string }} route - Route info
|
|
52
|
+
* @param {{ sourcePath?: string; _pathParams?: Record<string, string> }} route - Route info
|
|
70
53
|
* @param {string} projectRoot - Absolute path to the project root
|
|
54
|
+
* @param {{ config?: Record<string, unknown>; contentTypes?: Map<string, unknown[]> }} [projectContext] -
|
|
55
|
+
* Project-level context injected into all classes
|
|
71
56
|
*/
|
|
72
|
-
export async function resolvePrototypes(doc, route, projectRoot) {
|
|
57
|
+
export async function resolvePrototypes(doc, route, projectRoot, projectContext = {}) {
|
|
73
58
|
const imports = doc.imports ?? {};
|
|
74
59
|
const state = doc.state;
|
|
75
60
|
if (!state) return;
|
|
@@ -83,13 +68,13 @@ export async function resolvePrototypes(doc, route, projectRoot) {
|
|
|
83
68
|
|
|
84
69
|
// Look up in imports if no $src already set
|
|
85
70
|
if (!def.$src) {
|
|
86
|
-
const mapped = imports[def.$prototype]
|
|
71
|
+
const mapped = imports[def.$prototype];
|
|
87
72
|
if (!mapped) continue;
|
|
88
73
|
def.$src = mapped;
|
|
89
74
|
}
|
|
90
75
|
|
|
91
76
|
try {
|
|
92
|
-
const resolved = await resolveClassPrototype(def, route, projectRoot);
|
|
77
|
+
const resolved = await resolveClassPrototype(def, route, projectRoot, state, projectContext);
|
|
93
78
|
// Preserve timing metadata on the resolved value so compilePage() can strip it
|
|
94
79
|
if (def.timing && resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
95
80
|
resolved.timing = def.timing;
|
|
@@ -108,11 +93,13 @@ export async function resolvePrototypes(doc, route, projectRoot) {
|
|
|
108
93
|
* Resolve a single $prototype entry via its .class.json.
|
|
109
94
|
*
|
|
110
95
|
* @param {Record<string, any>} def - The state entry definition
|
|
111
|
-
* @param {{ sourcePath?: string }} route
|
|
96
|
+
* @param {{ sourcePath?: string; _pathParams?: Record<string, string> }} route
|
|
112
97
|
* @param {string} projectRoot
|
|
98
|
+
* @param {Record<string, unknown>} state - The full page state object
|
|
99
|
+
* @param {{ config?: Record<string, unknown>; contentTypes?: Map<string, unknown[]> }} projectContext
|
|
113
100
|
* @returns {Promise<any>} The resolved value
|
|
114
101
|
*/
|
|
115
|
-
async function resolveClassPrototype(def, route, projectRoot) {
|
|
102
|
+
async function resolveClassPrototype(def, route, projectRoot, state, projectContext) {
|
|
116
103
|
const src = def.$src;
|
|
117
104
|
|
|
118
105
|
// 1. Resolve .class.json path — handles both npm specifiers and relative paths
|
|
@@ -162,6 +149,14 @@ async function resolveClassPrototype(def, route, projectRoot) {
|
|
|
162
149
|
config.basePath = dirname(route.sourcePath);
|
|
163
150
|
}
|
|
164
151
|
|
|
152
|
+
// Inject universal context — available to all classes
|
|
153
|
+
config._project = {
|
|
154
|
+
config: projectContext.config ?? null,
|
|
155
|
+
contentTypes: projectContext.contentTypes ?? null,
|
|
156
|
+
root: projectRoot,
|
|
157
|
+
};
|
|
158
|
+
config._document = { route, state };
|
|
159
|
+
|
|
165
160
|
// 7. Instantiate and resolve
|
|
166
161
|
const instance = new ExportedClass(config);
|
|
167
162
|
if (typeof instance.resolve === "function") {
|
package/src/site/site-build.js
CHANGED
|
@@ -405,11 +405,14 @@ async function compilePage(
|
|
|
405
405
|
delete layoutDoc._pageHead;
|
|
406
406
|
delete layoutDoc._pageTitle;
|
|
407
407
|
|
|
408
|
-
// Inject $site and $page context
|
|
408
|
+
// Inject $site and $page context
|
|
409
409
|
injectContext(layoutDoc, projectConfig, route, contentTypes, projectRoot);
|
|
410
410
|
|
|
411
411
|
// Resolve generic $prototype entries via .class.json imports
|
|
412
|
-
await resolvePrototypes(layoutDoc, route, projectRoot
|
|
412
|
+
await resolvePrototypes(layoutDoc, route, projectRoot, {
|
|
413
|
+
config: projectConfig,
|
|
414
|
+
contentTypes,
|
|
415
|
+
});
|
|
413
416
|
|
|
414
417
|
// Build scope from resolved state so template strings in title/$head can be evaluated
|
|
415
418
|
const scope = buildInitialScope(layoutDoc.state ?? {});
|