@jxsuite/compiler 0.28.2 → 0.28.3
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/cli.js +159 -150
- package/dist/compiler.js +100 -95
- package/package.json +5 -5
- package/src/cli.ts +6 -6
- package/src/compile-cli.ts +6 -4
- package/src/compiler.ts +34 -23
- package/src/shared.ts +402 -231
- package/src/site/content-loader.ts +54 -29
- package/src/site/context-injection.ts +13 -9
- package/src/site/format-host.ts +26 -15
- package/src/site/head-merger.ts +37 -18
- package/src/site/image-cache.ts +13 -12
- package/src/site/image-optimizer.ts +30 -20
- package/src/site/image-transform.ts +144 -63
- package/src/site/layout-resolver.ts +33 -18
- package/src/site/pages-discovery.ts +61 -39
- package/src/site/prototype-resolver.ts +45 -21
- package/src/site/site-build.ts +212 -136
- package/src/site/site-loader.ts +70 -43
- package/src/targets/compile-class.ts +70 -51
- package/src/targets/compile-client.ts +271 -227
- package/src/targets/compile-element.ts +230 -146
- package/src/targets/compile-server.ts +9 -5
- package/src/targets/compile-static.ts +27 -19
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* prefix)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
16
|
+
import { parseJxDocument } from "@jxsuite/schema/parse";
|
|
17
|
+
import { extname, join, relative, resolve } from "node:path";
|
|
17
18
|
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
18
|
-
import type { JxDocument } from "@jxsuite/schema/types";
|
|
19
|
+
import type { JxDocument, JxPathsDef } from "@jxsuite/schema/types";
|
|
19
20
|
import type { ContentLoaderEntry } from "@jxsuite/parser/types";
|
|
20
21
|
|
|
21
22
|
interface Route {
|
|
@@ -43,7 +44,7 @@ export async function readPageDocument(
|
|
|
43
44
|
): Promise<JxDocument> {
|
|
44
45
|
const source = readFileSync(filePath, "utf8");
|
|
45
46
|
if (filePath.endsWith(".json")) {
|
|
46
|
-
return
|
|
47
|
+
return parseJxDocument(source, filePath);
|
|
47
48
|
}
|
|
48
49
|
const ext = extname(filePath).toLowerCase();
|
|
49
50
|
const entry = registry?.byExtension(ext, "parse");
|
|
@@ -71,8 +72,12 @@ export async function discoverPages(pagesDir: string, registry?: FormatRegistry)
|
|
|
71
72
|
|
|
72
73
|
// Sort: static routes first, then by specificity (more segments = more specific)
|
|
73
74
|
routes.sort((a, b) => {
|
|
74
|
-
if (a.isDynamic !== b.isDynamic)
|
|
75
|
-
|
|
75
|
+
if (a.isDynamic !== b.isDynamic) {
|
|
76
|
+
return a.isDynamic ? 1 : -1;
|
|
77
|
+
}
|
|
78
|
+
if (a.isCatchAll !== b.isCatchAll) {
|
|
79
|
+
return a.isCatchAll ? 1 : -1;
|
|
80
|
+
}
|
|
76
81
|
return a.urlPattern.localeCompare(b.urlPattern);
|
|
77
82
|
});
|
|
78
83
|
|
|
@@ -102,21 +107,29 @@ async function walkDir(
|
|
|
102
107
|
|
|
103
108
|
if (entry.isDirectory()) {
|
|
104
109
|
// Skip underscore-prefixed directories
|
|
105
|
-
if (entry.name.startsWith("_"))
|
|
110
|
+
if (entry.name.startsWith("_")) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
106
113
|
await walkDir(fullPath, pagesRoot, routes, pageExtensions, registry);
|
|
107
114
|
continue;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
// Only process .json and registered page-format files
|
|
111
118
|
const ext = extname(entry.name).toLowerCase();
|
|
112
|
-
if (!pageExtensions.has(ext))
|
|
119
|
+
if (!pageExtensions.has(ext)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
113
122
|
|
|
114
123
|
// Skip underscore-prefixed files (local components, not routes)
|
|
115
|
-
if (entry.name.startsWith("_"))
|
|
124
|
+
if (entry.name.startsWith("_")) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
116
127
|
|
|
117
128
|
const relativePath = relative(pagesRoot, fullPath);
|
|
118
129
|
const route = await fileToRoute(relativePath, fullPath, registry);
|
|
119
|
-
if (route)
|
|
130
|
+
if (route) {
|
|
131
|
+
routes.push(route);
|
|
132
|
+
}
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
135
|
|
|
@@ -136,7 +149,7 @@ async function fileToRoute(relativePath: string, absolutePath: string, registry?
|
|
|
136
149
|
// Normalize path separators
|
|
137
150
|
urlPath = urlPath.split("\\").join("/");
|
|
138
151
|
|
|
139
|
-
//
|
|
152
|
+
// Index files map to their parent directory
|
|
140
153
|
if (urlPath.endsWith("/index")) {
|
|
141
154
|
urlPath = urlPath.slice(0, -6) || "/";
|
|
142
155
|
} else if (urlPath === "index") {
|
|
@@ -144,7 +157,9 @@ async function fileToRoute(relativePath: string, absolutePath: string, registry?
|
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
// Ensure leading slash
|
|
147
|
-
if (!urlPath.startsWith("/"))
|
|
160
|
+
if (!urlPath.startsWith("/")) {
|
|
161
|
+
urlPath = `/${urlPath}`;
|
|
162
|
+
}
|
|
148
163
|
|
|
149
164
|
// Extract parameters from bracket syntax
|
|
150
165
|
const params: string[] = [];
|
|
@@ -152,7 +167,7 @@ async function fileToRoute(relativePath: string, absolutePath: string, registry?
|
|
|
152
167
|
let isCatchAll = false;
|
|
153
168
|
|
|
154
169
|
// Convert [param] → :param and [...param] → *
|
|
155
|
-
const urlPattern = urlPath.
|
|
170
|
+
const urlPattern = urlPath.replaceAll(
|
|
156
171
|
/\[\.\.\.(\w+)\]|\[(\w+)\]/g,
|
|
157
172
|
(match: string, spread: string, named: string) => {
|
|
158
173
|
if (spread) {
|
|
@@ -168,25 +183,25 @@ async function fileToRoute(relativePath: string, absolutePath: string, registry?
|
|
|
168
183
|
);
|
|
169
184
|
|
|
170
185
|
// Peek at the page to extract $layout if present
|
|
171
|
-
|
|
172
|
-
let $layout = null;
|
|
186
|
+
let $layout: string | null = null;
|
|
173
187
|
try {
|
|
174
188
|
const doc = await readPageDocument(absolutePath, registry);
|
|
175
|
-
|
|
176
|
-
|
|
189
|
+
const layout = doc.$layout;
|
|
190
|
+
if (typeof layout === "string") {
|
|
191
|
+
$layout = layout;
|
|
177
192
|
}
|
|
178
193
|
} catch {
|
|
179
194
|
// Skip unreadable files — will error during compilation
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
return {
|
|
183
|
-
|
|
184
|
-
sourcePath: absolutePath,
|
|
185
|
-
relativePath,
|
|
186
|
-
isDynamic,
|
|
198
|
+
$layout,
|
|
187
199
|
isCatchAll,
|
|
200
|
+
isDynamic,
|
|
188
201
|
params,
|
|
189
|
-
|
|
202
|
+
relativePath,
|
|
203
|
+
sourcePath: absolutePath,
|
|
204
|
+
urlPattern,
|
|
190
205
|
};
|
|
191
206
|
}
|
|
192
207
|
|
|
@@ -200,14 +215,15 @@ async function fileToRoute(relativePath: string, absolutePath: string, registry?
|
|
|
200
215
|
*
|
|
201
216
|
* @param {Route[]} routes - Discovered route table
|
|
202
217
|
* @param {string} projectRoot - Project root for resolving $ref paths
|
|
203
|
-
* @param {Map<string,
|
|
218
|
+
* @param {Map<string, ContentLoaderEntry[]>} [contentTypes] - Loaded content types (from
|
|
219
|
+
* content-loader)
|
|
204
220
|
* @param {FormatRegistry} [registry] - Format registry for reading non-JSON dynamic pages
|
|
205
221
|
* @returns {Promise<Route[]>} Expanded routes with concrete paths
|
|
206
222
|
*/
|
|
207
223
|
export async function expandDynamicRoutes(
|
|
208
224
|
routes: Route[],
|
|
209
225
|
projectRoot: string,
|
|
210
|
-
contentTypes
|
|
226
|
+
contentTypes = new Map<string, ContentLoaderEntry[]>(),
|
|
211
227
|
registry?: FormatRegistry,
|
|
212
228
|
) {
|
|
213
229
|
const expanded: Route[] = [];
|
|
@@ -237,18 +253,20 @@ export async function expandDynamicRoutes(
|
|
|
237
253
|
|
|
238
254
|
for (const pathEntry of pathEntries) {
|
|
239
255
|
let concreteUrl = route.urlPattern;
|
|
256
|
+
const params: Record<string, string> = {};
|
|
240
257
|
for (const [param, value] of Object.entries(pathEntry)) {
|
|
241
|
-
|
|
242
|
-
concreteUrl = concreteUrl.replace(
|
|
258
|
+
params[param] = String(value);
|
|
259
|
+
concreteUrl = concreteUrl.replace(`:${param}`, params[param]);
|
|
260
|
+
concreteUrl = concreteUrl.replace("*", params[param]);
|
|
243
261
|
}
|
|
244
262
|
|
|
245
263
|
expanded.push({
|
|
246
264
|
...route,
|
|
247
|
-
|
|
248
|
-
isDynamic: false,
|
|
265
|
+
_pathParams: params,
|
|
249
266
|
isCatchAll: false,
|
|
267
|
+
isDynamic: false,
|
|
250
268
|
params: [],
|
|
251
|
-
|
|
269
|
+
urlPattern: concreteUrl,
|
|
252
270
|
});
|
|
253
271
|
}
|
|
254
272
|
}
|
|
@@ -259,19 +277,23 @@ export async function expandDynamicRoutes(
|
|
|
259
277
|
/**
|
|
260
278
|
* Resolve $paths into an array of param objects.
|
|
261
279
|
*
|
|
262
|
-
* @param {
|
|
280
|
+
* @param {import("@jxsuite/schema/types").JxPathsDef} $paths - The $paths declaration
|
|
263
281
|
* @param {string} projectRoot
|
|
264
|
-
* @param {Map<string,
|
|
265
|
-
* @returns {Record<string,
|
|
282
|
+
* @param {Map<string, ContentLoaderEntry[]>} contentTypes
|
|
283
|
+
* @returns {Record<string, unknown>[]} Array of { paramName: value } objects
|
|
266
284
|
*/
|
|
267
|
-
function resolvePathEntries(
|
|
285
|
+
function resolvePathEntries(
|
|
286
|
+
$paths: JxPathsDef,
|
|
287
|
+
projectRoot: string,
|
|
288
|
+
contentTypes: Map<string, ContentLoaderEntry[]>,
|
|
289
|
+
): Record<string, unknown>[] {
|
|
268
290
|
// Legacy: array of param objects
|
|
269
291
|
if (Array.isArray($paths)) {
|
|
270
292
|
return $paths;
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
// Content type-based: { contentType: "blog", param: "slug", field: "id" }
|
|
274
|
-
if ($paths.contentType) {
|
|
296
|
+
if ("contentType" in $paths && $paths.contentType) {
|
|
275
297
|
const entries = contentTypes.get($paths.contentType);
|
|
276
298
|
if (!entries || entries.length === 0) {
|
|
277
299
|
console.warn(
|
|
@@ -289,20 +311,20 @@ function resolvePathEntries($paths: any, projectRoot: string, contentTypes: Map<
|
|
|
289
311
|
}
|
|
290
312
|
|
|
291
313
|
// Explicit values: { values: ["en", "fr"], param: "lang" }
|
|
292
|
-
if (Array.isArray($paths.values)) {
|
|
314
|
+
if ("values" in $paths && Array.isArray($paths.values)) {
|
|
293
315
|
const param = $paths.param ?? "value";
|
|
294
|
-
return $paths.values.map((v
|
|
316
|
+
return $paths.values.map((v) => ({ [param]: v }));
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
// Data file ref: { "$ref": "./data/products.json", param: "id", field: "sku" }
|
|
298
|
-
if ($paths.$ref) {
|
|
320
|
+
if ("$ref" in $paths && $paths.$ref) {
|
|
299
321
|
const filePath = resolve(projectRoot, $paths.$ref);
|
|
300
322
|
/** @type {Record<string, unknown>[]} */
|
|
301
323
|
let data;
|
|
302
324
|
try {
|
|
303
325
|
data = JSON.parse(readFileSync(filePath, "utf8"));
|
|
304
|
-
} catch (
|
|
305
|
-
const err =
|
|
326
|
+
} catch (error) {
|
|
327
|
+
const err = error as Error;
|
|
306
328
|
console.warn(`Warning: $paths.$ref could not load "${$paths.$ref}": ${err.message}`);
|
|
307
329
|
return [];
|
|
308
330
|
}
|
|
@@ -18,7 +18,15 @@ import { readFileSync } from "node:fs";
|
|
|
18
18
|
import { dirname, resolve } from "node:path";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import { importImplementation } from "./format-host.ts";
|
|
21
|
-
import
|
|
21
|
+
import { isPrototypeDef } from "@jxsuite/schema/guards";
|
|
22
|
+
import { errorMessage, parseClassDef } from "@jxsuite/schema/parse";
|
|
23
|
+
import type {
|
|
24
|
+
JsonObject,
|
|
25
|
+
JsonValue,
|
|
26
|
+
JxDocument,
|
|
27
|
+
JxMutableNode,
|
|
28
|
+
JxPrototypeDef,
|
|
29
|
+
} from "@jxsuite/schema/types";
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* Prototype names handled elsewhere (builtins + legacy content system). These are skipped by the
|
|
@@ -65,20 +73,30 @@ export async function resolvePrototypes(
|
|
|
65
73
|
} = {},
|
|
66
74
|
) {
|
|
67
75
|
const imports = doc.imports ?? {};
|
|
68
|
-
const state = doc
|
|
69
|
-
if (!state)
|
|
76
|
+
const { state } = doc;
|
|
77
|
+
if (!state) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
70
80
|
|
|
71
81
|
for (const [key, def] of Object.entries(state)) {
|
|
72
|
-
if (!def
|
|
73
|
-
|
|
82
|
+
if (!isPrototypeDef(def)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (SKIP_PROTOTYPES.has(def.$prototype)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
74
88
|
// Only resolve timing: "compiler" (or unset timing with a .class.json mapping).
|
|
75
89
|
// Leave timing: "server" and timing: "client" for their respective pipelines.
|
|
76
|
-
if (def.timing && def.timing !== "compiler")
|
|
90
|
+
if (def.timing && def.timing !== "compiler") {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
77
93
|
|
|
78
94
|
// Look up in imports if no $src already set
|
|
79
95
|
if (!def.$src) {
|
|
80
96
|
const mapped = imports[def.$prototype];
|
|
81
|
-
if (!mapped)
|
|
97
|
+
if (!mapped) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
82
100
|
def.$src = mapped;
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -86,13 +104,13 @@ export async function resolvePrototypes(
|
|
|
86
104
|
const resolved = await resolveClassPrototype(def, route, projectRoot, state, projectContext);
|
|
87
105
|
// Preserve timing metadata on the resolved value so compilePage() can strip it
|
|
88
106
|
if (def.timing && resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
89
|
-
(resolved as
|
|
107
|
+
(resolved as JsonObject).timing = def.timing;
|
|
90
108
|
}
|
|
91
109
|
state[key] = resolved;
|
|
92
|
-
} catch (
|
|
110
|
+
} catch (error) {
|
|
93
111
|
console.warn(
|
|
94
112
|
`prototype-resolver: failed to resolve "${key}" ($prototype: "${def.$prototype}"):`,
|
|
95
|
-
(
|
|
113
|
+
errorMessage(error),
|
|
96
114
|
);
|
|
97
115
|
}
|
|
98
116
|
}
|
|
@@ -101,15 +119,15 @@ export async function resolvePrototypes(
|
|
|
101
119
|
/**
|
|
102
120
|
* Resolve a single $prototype entry via its .class.json.
|
|
103
121
|
*
|
|
104
|
-
* @param {
|
|
122
|
+
* @param {JxPrototypeDef} def - The state entry definition
|
|
105
123
|
* @param {{ sourcePath?: string; _pathParams?: Record<string, string> }} route
|
|
106
124
|
* @param {string} projectRoot
|
|
107
125
|
* @param {Record<string, unknown>} state - The full page state object
|
|
108
126
|
* @param {{ config?: Record<string, unknown>; contentTypes?: Map<string, unknown[]> }} projectContext
|
|
109
|
-
* @returns {Promise<
|
|
127
|
+
* @returns {Promise<JsonValue>} The resolved value
|
|
110
128
|
*/
|
|
111
129
|
async function resolveClassPrototype(
|
|
112
|
-
def:
|
|
130
|
+
def: JxPrototypeDef,
|
|
113
131
|
route: { sourcePath?: string; _pathParams?: Record<string, string> },
|
|
114
132
|
projectRoot: string,
|
|
115
133
|
state: Record<string, unknown>,
|
|
@@ -119,6 +137,9 @@ async function resolveClassPrototype(
|
|
|
119
137
|
},
|
|
120
138
|
) {
|
|
121
139
|
const src = def.$src;
|
|
140
|
+
if (!src) {
|
|
141
|
+
throw new Error(`$prototype "${def.$prototype}" has no $src to resolve`);
|
|
142
|
+
}
|
|
122
143
|
|
|
123
144
|
// 1. Resolve .class.json path — handles both npm specifiers and relative paths
|
|
124
145
|
let classJsonPath;
|
|
@@ -128,14 +149,14 @@ async function resolveClassPrototype(
|
|
|
128
149
|
? resolve(dirname(route.sourcePath), src)
|
|
129
150
|
: resolve(projectRoot, src);
|
|
130
151
|
} else {
|
|
131
|
-
//
|
|
152
|
+
// Npm/bare specifier — use createRequire from the project root to walk node_modules
|
|
132
153
|
const require = createRequire(resolve(projectRoot, "package.json"));
|
|
133
154
|
classJsonPath = require.resolve(src);
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
// 2. Read and parse .class.json
|
|
137
|
-
const classJsonText = readFileSync(classJsonPath, "
|
|
138
|
-
const classDef =
|
|
158
|
+
const classJsonText = readFileSync(classJsonPath, "utf8");
|
|
159
|
+
const classDef = parseClassDef(classJsonText, classJsonPath);
|
|
139
160
|
|
|
140
161
|
if (!classDef.$implementation) {
|
|
141
162
|
throw new Error(`${src} has no $implementation field`);
|
|
@@ -158,9 +179,11 @@ async function resolveClassPrototype(
|
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
// 6. Build config — filter out reserved keys
|
|
161
|
-
const config: Record<string,
|
|
182
|
+
const config: Record<string, unknown> = {};
|
|
162
183
|
for (const [k, v] of Object.entries(def)) {
|
|
163
|
-
if (!RESERVED_KEYS.has(k))
|
|
184
|
+
if (!RESERVED_KEYS.has(k)) {
|
|
185
|
+
config[k] = v;
|
|
186
|
+
}
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
// Auto-set basePath from the page's directory if the config has `src` but no `basePath`
|
|
@@ -176,10 +199,11 @@ async function resolveClassPrototype(
|
|
|
176
199
|
};
|
|
177
200
|
config._document = { route, state };
|
|
178
201
|
|
|
179
|
-
// 7. Instantiate and resolve
|
|
202
|
+
// 7. Instantiate and resolve. Trust boundary: external classes contractually
|
|
203
|
+
// Return JSON-serializable data (the result is baked into the compiled page).
|
|
180
204
|
const instance = new ExportedClass(config);
|
|
181
205
|
if (typeof instance.resolve === "function") {
|
|
182
|
-
return await instance.resolve();
|
|
206
|
+
return (await instance.resolve()) as JsonValue;
|
|
183
207
|
}
|
|
184
|
-
return instance;
|
|
208
|
+
return instance as unknown as JsonValue;
|
|
185
209
|
}
|