@jxsuite/compiler 0.28.0 → 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 +166 -249
- package/dist/compiler.js +100 -188
- package/package.json +5 -5
- package/src/cli.ts +6 -6
- package/src/compile-cli.ts +6 -4
- package/src/compiler.ts +35 -29
- 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 +32 -23
- package/src/site/image-transform.ts +172 -81
- 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 +232 -183
- package/src/site/site-loader.ts +71 -52
- 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 +16 -62
- package/src/targets/compile-static.ts +27 -19
- package/src/targets/compile-image-endpoint.ts +0 -138
package/src/site/site-build.ts
CHANGED
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
writeFileSync,
|
|
13
12
|
copyFileSync,
|
|
14
|
-
mkdirSync,
|
|
15
|
-
existsSync,
|
|
16
|
-
rmSync,
|
|
17
13
|
cpSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
18
16
|
readdirSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync,
|
|
19
19
|
} from "node:fs";
|
|
20
|
-
import {
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { isMappedArray, isRef } from "@jxsuite/schema/guards";
|
|
21
22
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
22
23
|
import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
|
|
23
24
|
import { buildProjectFormatRegistry } from "./format-host.ts";
|
|
@@ -25,39 +26,42 @@ import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
|
25
26
|
import { resolveLayout } from "./layout-resolver.ts";
|
|
26
27
|
import { mergeHead, renderHead } from "./head-merger.ts";
|
|
27
28
|
import { injectContext } from "./context-injection.ts";
|
|
28
|
-
import { compile, compileServer, compileSiteServer
|
|
29
|
+
import { compile, compileServer, compileSiteServer } from "../compiler.ts";
|
|
29
30
|
import { compileElement } from "../targets/compile-element.ts";
|
|
30
31
|
import {
|
|
32
|
+
DEFAULT_LIT_HTML_SRC,
|
|
33
|
+
DEFAULT_REACTIVITY_SRC,
|
|
34
|
+
buildComponentCSS,
|
|
31
35
|
buildInitialScope,
|
|
32
|
-
|
|
36
|
+
collectServerEntries,
|
|
37
|
+
collectStyles,
|
|
33
38
|
evaluateStaticTemplate,
|
|
34
|
-
preRenderComponentHtml,
|
|
35
39
|
isComponentFullyStatic,
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
isTemplateString,
|
|
41
|
+
preRenderComponentHtml,
|
|
38
42
|
renderStaticNode,
|
|
39
|
-
resolveStaticValue,
|
|
40
43
|
resolveRefValue,
|
|
41
|
-
|
|
42
|
-
DEFAULT_LIT_HTML_SRC,
|
|
44
|
+
resolveStaticValue,
|
|
43
45
|
} from "../shared.ts";
|
|
44
|
-
import {
|
|
46
|
+
import { loadContentConfig, loadContentTypes, resolveContentTypeRefs } from "./content-loader.ts";
|
|
45
47
|
import { resolvePrototypes } from "./prototype-resolver.ts";
|
|
46
48
|
import { transformImageNodes } from "./image-transform.ts";
|
|
47
|
-
import { loadCache, saveCache
|
|
48
|
-
import { compilePagesImageFunction } from "../targets/compile-image-endpoint.ts";
|
|
49
|
+
import { getImageCacheDir, loadCache, saveCache } from "./image-cache.ts";
|
|
49
50
|
import type { ImageConfig } from "./image-optimizer.ts";
|
|
50
51
|
import type { ImageMetaCache } from "./image-transform.ts";
|
|
51
52
|
import type {
|
|
53
|
+
JsonValue,
|
|
54
|
+
JxAttributeValue,
|
|
52
55
|
JxElement,
|
|
53
|
-
JxMutableNode,
|
|
54
|
-
JxStyle,
|
|
55
56
|
JxHeadEntry,
|
|
57
|
+
JxMutableNode,
|
|
56
58
|
JxStateDefinition,
|
|
59
|
+
JxStyle,
|
|
57
60
|
ProjectConfig,
|
|
58
61
|
} from "@jxsuite/schema/types";
|
|
59
62
|
import type { ContentLoaderEntry } from "@jxsuite/parser/types";
|
|
60
63
|
import type { SiteRoute } from "../types.ts";
|
|
64
|
+
import type { CacheManifest } from "./image-cache.js";
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
67
|
* Build an entire Jx site from a project directory.
|
|
@@ -100,7 +104,7 @@ export async function buildSite(
|
|
|
100
104
|
|
|
101
105
|
// ── 2. Clean output directory ───────────────────────────────────────────
|
|
102
106
|
if (clean && existsSync(outDir)) {
|
|
103
|
-
rmSync(outDir, {
|
|
107
|
+
rmSync(outDir, { force: true, recursive: true });
|
|
104
108
|
}
|
|
105
109
|
mkdirSync(outDir, { recursive: true });
|
|
106
110
|
|
|
@@ -134,8 +138,8 @@ export async function buildSite(
|
|
|
134
138
|
// ── 5. Compile site components ──────────────────────────────────────────
|
|
135
139
|
const componentsDir = resolve(projectRoot, "components");
|
|
136
140
|
const compiledComponentTags: string[] = [];
|
|
137
|
-
const componentCSS
|
|
138
|
-
const componentDefs
|
|
141
|
+
const componentCSS = new Map<string, string>(); // TagName → CSS text
|
|
142
|
+
const componentDefs = new Map<string, JxElement>(); // TagName → parsed component definition
|
|
139
143
|
if (existsSync(componentsDir)) {
|
|
140
144
|
log("Compiling components...");
|
|
141
145
|
const componentExtensions = [".json", ...formatRegistry.documentExtensions("component")];
|
|
@@ -149,14 +153,16 @@ export async function buildSite(
|
|
|
149
153
|
try {
|
|
150
154
|
const componentPath = resolve(componentsDir, file);
|
|
151
155
|
const result = await compileElement(componentPath, {
|
|
152
|
-
$media: projectConfig.$media,
|
|
156
|
+
...(projectConfig.$media ? { $media: projectConfig.$media } : {}),
|
|
153
157
|
formats: formatRegistry,
|
|
154
158
|
});
|
|
155
159
|
for (const f of result.files) {
|
|
156
160
|
const outName = f.path.includes("/") ? (f.path.split("/").pop() as string) : f.path;
|
|
157
161
|
writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
|
|
158
|
-
if (f.tagName)
|
|
159
|
-
|
|
162
|
+
if (f.tagName) {
|
|
163
|
+
compiledComponentTags.push(f.tagName);
|
|
164
|
+
}
|
|
165
|
+
fileCount += 1;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
// Pre-render component HTML scaffold and CSS sidecar
|
|
@@ -167,11 +173,11 @@ export async function buildSite(
|
|
|
167
173
|
if (css) {
|
|
168
174
|
componentCSS.set(doc.tagName, css);
|
|
169
175
|
writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
|
|
170
|
-
fileCount
|
|
176
|
+
fileCount += 1;
|
|
171
177
|
}
|
|
172
178
|
}
|
|
173
|
-
} catch (
|
|
174
|
-
const err =
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const err = error as Error;
|
|
175
181
|
errors.push(`Error compiling component ${file}: ${err.message}`);
|
|
176
182
|
console.error(`Error compiling component ${file}: ${err.message}`);
|
|
177
183
|
}
|
|
@@ -188,7 +194,7 @@ export async function buildSite(
|
|
|
188
194
|
for (const [, doc] of componentDefs) {
|
|
189
195
|
const entries = collectServerEntries(doc);
|
|
190
196
|
for (const entry of entries) {
|
|
191
|
-
const resolvedSrc =
|
|
197
|
+
const resolvedSrc = `./components/${entry.src.replace(/^\.\//, "")}`;
|
|
192
198
|
siteServerEntries.push({
|
|
193
199
|
exportName: entry.exportName,
|
|
194
200
|
src: resolvedSrc,
|
|
@@ -205,11 +211,9 @@ export async function buildSite(
|
|
|
205
211
|
const imageMetaCache = cfImages ? new Map() : null;
|
|
206
212
|
if (cfImages) {
|
|
207
213
|
console.log(
|
|
208
|
-
`images.service is "cloudflare" —
|
|
209
|
-
`
|
|
210
|
-
|
|
211
|
-
? ` and an assets binding ("assets": { "binding": "ASSETS" }).`
|
|
212
|
-
: `.`),
|
|
214
|
+
`images.service is "cloudflare" — srcsets use /cdn-cgi/image transform URLs. ` +
|
|
215
|
+
`Ensure Image Transformations are enabled for your zone (Cloudflare dashboard → ` +
|
|
216
|
+
`Images → Transformations); these URLs do not work on *.pages.dev / *.workers.dev previews.`,
|
|
213
217
|
);
|
|
214
218
|
}
|
|
215
219
|
|
|
@@ -228,9 +232,11 @@ export async function buildSite(
|
|
|
228
232
|
);
|
|
229
233
|
|
|
230
234
|
// Determine which component tags are fully static (for script omission)
|
|
231
|
-
const staticTags
|
|
235
|
+
const staticTags = new Set<string>();
|
|
232
236
|
for (const [tag, def] of componentDefs) {
|
|
233
|
-
if (isComponentFullyStatic(def))
|
|
237
|
+
if (isComponentFullyStatic(def)) {
|
|
238
|
+
staticTags.add(tag);
|
|
239
|
+
}
|
|
234
240
|
}
|
|
235
241
|
|
|
236
242
|
// Inject component CSS and JS scripts
|
|
@@ -247,29 +253,33 @@ export async function buildSite(
|
|
|
247
253
|
const outPath = routeToOutputPath(route.urlPattern, outDir, trailingSlash);
|
|
248
254
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
249
255
|
writeFileSync(outPath, result.html, "utf8");
|
|
250
|
-
fileCount
|
|
256
|
+
fileCount += 1;
|
|
251
257
|
|
|
252
258
|
// Write serialized export sidecars alongside HTML (formats with exportTarget: true)
|
|
253
259
|
for (const fmt of formatRegistry.withCapability("serialize")) {
|
|
254
|
-
if (!fmt.exportTarget)
|
|
260
|
+
if (!fmt.exportTarget) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
255
263
|
try {
|
|
256
264
|
const content = (await fmt.call("serialize", result.doc, {
|
|
257
|
-
|
|
265
|
+
buildScope: (state: Record<string, JxStateDefinition>) =>
|
|
266
|
+
buildInitialScope(state, null),
|
|
258
267
|
componentDefs,
|
|
259
268
|
evaluateTemplate: (value: string, scope: Record<string, unknown>) => {
|
|
260
|
-
if (!isTemplateString(value))
|
|
269
|
+
if (!isTemplateString(value)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
261
272
|
return evaluateStaticTemplate(value, scope) ?? value;
|
|
262
273
|
},
|
|
263
|
-
|
|
264
|
-
buildInitialScope(state, null),
|
|
274
|
+
mode: "export",
|
|
265
275
|
})) as string;
|
|
266
276
|
if (content) {
|
|
267
277
|
const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]);
|
|
268
278
|
writeFileSync(sidecarPath, content, "utf8");
|
|
269
|
-
fileCount
|
|
279
|
+
fileCount += 1;
|
|
270
280
|
}
|
|
271
|
-
} catch (
|
|
272
|
-
const err =
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const err = error as Error;
|
|
273
283
|
errors.push(`Error exporting ${fmt.name} for ${route.urlPattern}: ${err.message}`);
|
|
274
284
|
}
|
|
275
285
|
}
|
|
@@ -279,17 +289,17 @@ export async function buildSite(
|
|
|
279
289
|
const filePath = resolve(dirname(outPath), file.path);
|
|
280
290
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
281
291
|
writeFileSync(filePath, file.content, "utf8");
|
|
282
|
-
fileCount
|
|
292
|
+
fileCount += 1;
|
|
283
293
|
}
|
|
284
294
|
|
|
285
295
|
// Write server handler if present
|
|
286
296
|
if (result.serverHandler) {
|
|
287
297
|
const serverPath = resolve(dirname(outPath), "_server.js");
|
|
288
298
|
writeFileSync(serverPath, result.serverHandler, "utf8");
|
|
289
|
-
fileCount
|
|
299
|
+
fileCount += 1;
|
|
290
300
|
}
|
|
291
|
-
} catch (
|
|
292
|
-
const err =
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const err = error as Error;
|
|
293
303
|
const msg = `Error compiling ${route.urlPattern}: ${err.message}`;
|
|
294
304
|
errors.push(msg);
|
|
295
305
|
console.error(msg);
|
|
@@ -313,32 +323,39 @@ export async function buildSite(
|
|
|
313
323
|
|
|
314
324
|
// ── 6c. Generate site-wide server worker ────────────────────────────────
|
|
315
325
|
if (projectConfig.build.adapter) {
|
|
316
|
-
const adapter = projectConfig.build
|
|
326
|
+
const { adapter } = projectConfig.build;
|
|
317
327
|
log("Generating site-wide server worker...");
|
|
318
328
|
|
|
319
329
|
const deduped = new Map();
|
|
320
330
|
for (const entry of siteServerEntries) {
|
|
321
|
-
if (!deduped.has(entry.exportName))
|
|
331
|
+
if (!deduped.has(entry.exportName)) {
|
|
332
|
+
deduped.set(entry.exportName, entry);
|
|
333
|
+
}
|
|
322
334
|
}
|
|
323
335
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
336
|
+
// Cloudflare Pages uses advanced mode (_worker.js inside the build output) — the
|
|
337
|
+
// Functions/ directory convention only works from the project root, not from dist/.
|
|
338
|
+
// A static-only Pages site needs no worker at all.
|
|
339
|
+
const skipWorker = adapter === "cloudflare-pages" && deduped.size === 0;
|
|
340
|
+
const workerSource = skipWorker ? null : compileSiteServer([...deduped.values()], { adapter });
|
|
341
|
+
|
|
342
|
+
if (workerSource) {
|
|
343
|
+
const workerName = adapter === "cloudflare-pages" ? "_worker.js" : "worker.js";
|
|
344
|
+
writeFileSync(resolve(outDir, workerName), workerSource, "utf8");
|
|
345
|
+
fileCount += 1;
|
|
346
|
+
log(` Generated dist/${workerName} (${deduped.size} server function(s))`);
|
|
347
|
+
|
|
348
|
+
if (adapter === "cloudflare-pages") {
|
|
349
|
+
// Only invoke the worker for server routes; everything else stays static.
|
|
350
|
+
writeFileSync(
|
|
351
|
+
resolve(outDir, "_routes.json"),
|
|
352
|
+
`${JSON.stringify({ exclude: [], include: ["/_jx/*"], version: 1 }, null, 2)}\n`,
|
|
353
|
+
"utf8",
|
|
331
354
|
);
|
|
355
|
+
fileCount += 1;
|
|
332
356
|
}
|
|
333
357
|
|
|
334
|
-
|
|
335
|
-
const fullPath = resolve(outDir, filePath);
|
|
336
|
-
mkdirSync(resolve(fullPath, ".."), { recursive: true });
|
|
337
|
-
writeFileSync(fullPath, source, "utf8");
|
|
338
|
-
fileCount++;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Copy server source files so function imports resolve
|
|
358
|
+
// Copy server source files into dist/components/ so worker imports resolve
|
|
342
359
|
const distComponentsDir = resolve(outDir, "components");
|
|
343
360
|
mkdirSync(distComponentsDir, { recursive: true });
|
|
344
361
|
for (const { src } of deduped.values()) {
|
|
@@ -348,35 +365,6 @@ export async function buildSite(
|
|
|
348
365
|
copyFileSync(srcFile, destFile);
|
|
349
366
|
}
|
|
350
367
|
}
|
|
351
|
-
|
|
352
|
-
if (functions.size > 0) {
|
|
353
|
-
log(` Generated ${functions.size} Pages function(s) in dist/functions/`);
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
const workerSource = compileSiteServer([...deduped.values()], {
|
|
357
|
-
adapter,
|
|
358
|
-
images:
|
|
359
|
-
cfImages && adapter === "cloudflare-workers"
|
|
360
|
-
? (projectConfig.images as ImageConfig)
|
|
361
|
-
: null,
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (workerSource) {
|
|
365
|
-
const workerPath = resolve(outDir, "worker.js");
|
|
366
|
-
writeFileSync(workerPath, workerSource, "utf8");
|
|
367
|
-
fileCount++;
|
|
368
|
-
log(` Generated dist/worker.js (${deduped.size} server function(s))`);
|
|
369
|
-
|
|
370
|
-
// Copy server source files into dist/components/ so worker imports resolve
|
|
371
|
-
const distComponentsDir = resolve(outDir, "components");
|
|
372
|
-
for (const { src } of deduped.values()) {
|
|
373
|
-
const srcFile = resolve(projectRoot, src.replace(/^\.\//, ""));
|
|
374
|
-
const destFile = resolve(distComponentsDir, src.replace(/^\.\/components\//, ""));
|
|
375
|
-
if (existsSync(srcFile)) {
|
|
376
|
-
copyFileSync(srcFile, destFile);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
368
|
}
|
|
381
369
|
}
|
|
382
370
|
|
|
@@ -410,7 +398,7 @@ export async function buildSite(
|
|
|
410
398
|
log(` ${errors.length} error(s)`);
|
|
411
399
|
}
|
|
412
400
|
|
|
413
|
-
return {
|
|
401
|
+
return { errors, files: fileCount, routes: routes.length };
|
|
414
402
|
}
|
|
415
403
|
|
|
416
404
|
/**
|
|
@@ -436,9 +424,9 @@ async function compilePage(
|
|
|
436
424
|
route: SiteRoute,
|
|
437
425
|
projectConfig: ProjectConfig,
|
|
438
426
|
projectRoot: string,
|
|
439
|
-
contentTypes
|
|
440
|
-
imageCache:
|
|
441
|
-
componentDefs
|
|
427
|
+
contentTypes = new Map<string, ContentLoaderEntry[]>(),
|
|
428
|
+
imageCache: CacheManifest | null = null,
|
|
429
|
+
componentDefs = new Map<string, JxElement>(),
|
|
442
430
|
imageMetaCache: ImageMetaCache | null = null,
|
|
443
431
|
formatRegistry?: FormatRegistry,
|
|
444
432
|
) {
|
|
@@ -449,9 +437,9 @@ async function compilePage(
|
|
|
449
437
|
const layoutDoc = resolveLayout(pageDoc, projectConfig, projectRoot);
|
|
450
438
|
|
|
451
439
|
// Extract head arrays before they get lost in the merge
|
|
452
|
-
const pageHead = pageDoc.$head ?? layoutDoc._pageHead ??
|
|
453
|
-
const layoutHead = layoutDoc.$head ?? [];
|
|
454
|
-
const pageTitle = pageDoc.title ?? layoutDoc._pageTitle ??
|
|
440
|
+
const pageHead = (pageDoc.$head ?? layoutDoc._pageHead ?? []) as JxHeadEntry[];
|
|
441
|
+
const layoutHead = (layoutDoc.$head ?? []) as JxHeadEntry[];
|
|
442
|
+
const pageTitle = (pageDoc.title ?? layoutDoc._pageTitle ?? null) as string | null;
|
|
455
443
|
|
|
456
444
|
// Clean up internal properties
|
|
457
445
|
delete layoutDoc._pageHead;
|
|
@@ -480,19 +468,28 @@ async function compilePage(
|
|
|
480
468
|
const resolvedLayoutHead = resolveHeadTemplates(layoutHead, scope);
|
|
481
469
|
|
|
482
470
|
// Resolve template strings in the document tree (innerHTML, textContent, style, attributes)
|
|
483
|
-
//
|
|
471
|
+
// So that timing: "compiler" data is baked into the static HTML
|
|
484
472
|
resolveDocTemplates(layoutDoc, scope);
|
|
485
473
|
|
|
486
|
-
// Expand registered custom elements (apply $props, pre-render, mark static/prerendered)
|
|
487
|
-
|
|
474
|
+
// Expand registered custom elements (apply $props, pre-render, mark static/prerendered).
|
|
475
|
+
// Slot children are serialized to HTML strings during expansion — before compileStyles can walk
|
|
476
|
+
// Them — so their static styles are collected here and injected as a page style block below.
|
|
477
|
+
const slotCss: SlotCssCollector = {
|
|
478
|
+
counter: { n: 0 },
|
|
479
|
+
media: { ...projectConfig.$media, ...layoutDoc.$media },
|
|
480
|
+
rules: [],
|
|
481
|
+
};
|
|
482
|
+
expandComponents(layoutDoc, componentDefs, slotCss);
|
|
488
483
|
|
|
489
484
|
// Strip resolved timing: "compiler" state entries — they're now baked into the tree
|
|
490
|
-
//
|
|
485
|
+
// And keeping them would cause isDynamic() to misclassify the page as dynamic.
|
|
491
486
|
// Also strip resolved content arrays (from ContentCollection) that have been
|
|
492
|
-
//
|
|
487
|
+
// Baked into unrolled map templates.
|
|
493
488
|
if (layoutDoc.state) {
|
|
494
489
|
for (const [key, def] of Object.entries(layoutDoc.state)) {
|
|
495
|
-
if (key === "$site" || key === "$page")
|
|
490
|
+
if (key === "$site" || key === "$page") {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
496
493
|
if (
|
|
497
494
|
def &&
|
|
498
495
|
typeof def === "object" &&
|
|
@@ -512,7 +509,7 @@ async function compilePage(
|
|
|
512
509
|
// Merge $head from site + layout + page
|
|
513
510
|
const mergedHead = mergeHead(resolvedSiteHead, resolvedLayoutHead, resolvedPageHead, {
|
|
514
511
|
title,
|
|
515
|
-
charset: projectConfig.defaults?.charset ?? "
|
|
512
|
+
charset: projectConfig.defaults?.charset ?? "utf8",
|
|
516
513
|
...(projectConfig.name != null && { siteName: projectConfig.name }),
|
|
517
514
|
...(projectConfig.url != null && { siteUrl: projectConfig.url }),
|
|
518
515
|
pageUrl: route.urlPattern,
|
|
@@ -536,11 +533,19 @@ async function compilePage(
|
|
|
536
533
|
|
|
537
534
|
// Compile the document using the existing compiler
|
|
538
535
|
const result = await compile(layoutDoc, {
|
|
539
|
-
title,
|
|
540
536
|
lang: projectConfig.defaults?.lang ?? "en",
|
|
541
537
|
projectStyle: projectConfig.style ?? null,
|
|
538
|
+
title,
|
|
542
539
|
});
|
|
543
540
|
|
|
541
|
+
// Inject CSS rules collected from component slot content
|
|
542
|
+
if (slotCss.rules.length > 0) {
|
|
543
|
+
result.html = result.html.replace(
|
|
544
|
+
"</head>",
|
|
545
|
+
`<style>\n${slotCss.rules.join("\n")}\n</style>\n</head>`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
544
549
|
// Post-process: inject merged <head> content into the compiled HTML
|
|
545
550
|
result.html = injectHead(result.html, mergedHead, projectConfig.defaults?.lang ?? "en");
|
|
546
551
|
|
|
@@ -549,7 +554,7 @@ async function compilePage(
|
|
|
549
554
|
(e: JxElement | string) => typeof e === "string" && !e.startsWith("./") && !e.startsWith("../"),
|
|
550
555
|
);
|
|
551
556
|
if (npmElements.length > 0) {
|
|
552
|
-
result.html = injectNpmElementScripts(result.html,
|
|
557
|
+
result.html = injectNpmElementScripts(result.html, npmElements as string[]);
|
|
553
558
|
}
|
|
554
559
|
|
|
555
560
|
// Compile server handler if applicable (skip when provider bundles site-wide)
|
|
@@ -566,10 +571,10 @@ async function compilePage(
|
|
|
566
571
|
}
|
|
567
572
|
|
|
568
573
|
return {
|
|
569
|
-
|
|
574
|
+
doc: layoutDoc,
|
|
570
575
|
files: result.files,
|
|
576
|
+
html: result.html,
|
|
571
577
|
serverHandler,
|
|
572
|
-
doc: layoutDoc,
|
|
573
578
|
};
|
|
574
579
|
}
|
|
575
580
|
|
|
@@ -582,7 +587,9 @@ async function compilePage(
|
|
|
582
587
|
*/
|
|
583
588
|
function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string, unknown>) {
|
|
584
589
|
return headEntries.map((entry: JxHeadEntry) => {
|
|
585
|
-
if (!entry || typeof entry !== "object")
|
|
590
|
+
if (!entry || typeof entry !== "object") {
|
|
591
|
+
return entry;
|
|
592
|
+
}
|
|
586
593
|
const resolved = { ...entry };
|
|
587
594
|
if (resolved.attributes) {
|
|
588
595
|
resolved.attributes = { ...resolved.attributes };
|
|
@@ -611,7 +618,9 @@ function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string,
|
|
|
611
618
|
*/
|
|
612
619
|
function resolveHeadBareSpecifiers(headEntries: JxHeadEntry[]) {
|
|
613
620
|
return headEntries.map((entry: JxHeadEntry) => {
|
|
614
|
-
if (!entry || typeof entry !== "object" || !entry.attributes)
|
|
621
|
+
if (!entry || typeof entry !== "object" || !entry.attributes) {
|
|
622
|
+
return entry;
|
|
623
|
+
}
|
|
615
624
|
const resolved = { ...entry, attributes: { ...entry.attributes } };
|
|
616
625
|
for (const key of ["href", "src"]) {
|
|
617
626
|
const val = resolved.attributes[key];
|
|
@@ -642,41 +651,46 @@ function isBareSpecifier(s: string) {
|
|
|
642
651
|
/**
|
|
643
652
|
* Deep-clone a map template, resolving template strings and $ref values against the given scope.
|
|
644
653
|
*
|
|
645
|
-
* @param {
|
|
654
|
+
* @param {JxElement} template
|
|
646
655
|
* @param {Record<string, unknown>} scope
|
|
647
|
-
* @returns {
|
|
656
|
+
* @returns {JxElement}
|
|
648
657
|
*/
|
|
649
|
-
function expandMapTemplate(template:
|
|
650
|
-
if (!template || typeof template !== "object")
|
|
651
|
-
|
|
658
|
+
function expandMapTemplate(template: JxElement, scope: Record<string, unknown>): JxElement {
|
|
659
|
+
if (!template || typeof template !== "object") {
|
|
660
|
+
return template;
|
|
661
|
+
}
|
|
662
|
+
const node = {} as JxElement;
|
|
652
663
|
for (const [k, v] of Object.entries(template)) {
|
|
653
664
|
if (k === "children" && Array.isArray(v)) {
|
|
654
665
|
node.children = v.map((child) => {
|
|
655
|
-
if (typeof child === "string")
|
|
666
|
+
if (typeof child === "string") {
|
|
667
|
+
return child;
|
|
668
|
+
}
|
|
656
669
|
return expandMapTemplate(/** @type {JxMutableNode} */ child, scope);
|
|
657
670
|
});
|
|
658
671
|
} else if (k === "style" && v && typeof v === "object") {
|
|
659
|
-
const style = { ...v
|
|
672
|
+
const style: JxStyle = { ...(v as JxStyle) };
|
|
660
673
|
for (const [sk, sv] of Object.entries(style)) {
|
|
661
|
-
if (
|
|
662
|
-
|
|
674
|
+
if (isTemplateString(sv)) {
|
|
675
|
+
// Template evaluation yields a substituted scalar for style values.
|
|
676
|
+
style[sk] = (evaluateMapTemplate(sv, scope) as string | number | undefined) ?? sv;
|
|
663
677
|
}
|
|
664
678
|
}
|
|
665
679
|
node.style = style;
|
|
666
680
|
} else if (k === "attributes" && v && typeof v === "object") {
|
|
667
|
-
const attrs = { ...v
|
|
681
|
+
const attrs = { ...(v as Record<string, JxAttributeValue>) };
|
|
668
682
|
for (const [ak, av] of Object.entries(attrs)) {
|
|
669
|
-
if (
|
|
670
|
-
attrs[ak] = evaluateMapTemplate(av, scope) ?? av;
|
|
683
|
+
if (isTemplateString(av)) {
|
|
684
|
+
attrs[ak] = (evaluateMapTemplate(av, scope) as JxAttributeValue | undefined) ?? av;
|
|
671
685
|
}
|
|
672
686
|
}
|
|
673
687
|
node.attributes = attrs;
|
|
674
688
|
} else if (k === "$props" && v && typeof v === "object") {
|
|
675
|
-
const props = { ...v
|
|
689
|
+
const props = { ...(v as Record<string, JsonValue>) };
|
|
676
690
|
for (const [pk, pv] of Object.entries(props)) {
|
|
677
|
-
if (
|
|
678
|
-
const resolved = evaluateMapTemplate(pv, scope);
|
|
679
|
-
//
|
|
691
|
+
if (isTemplateString(pv)) {
|
|
692
|
+
const resolved = evaluateMapTemplate(pv, scope) as JsonValue | undefined;
|
|
693
|
+
// Null = evaluation error → keep template string; undefined = missing data → use null
|
|
680
694
|
props[pk] = resolved !== null ? (resolved ?? null) : pv;
|
|
681
695
|
}
|
|
682
696
|
}
|
|
@@ -722,14 +736,16 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
|
|
|
722
736
|
* @param {Record<string, unknown>} scope
|
|
723
737
|
*/
|
|
724
738
|
function resolveDocTemplates(node: JxElement | string, scope: Record<string, unknown>) {
|
|
725
|
-
if (!node || typeof node !== "object")
|
|
739
|
+
if (!node || typeof node !== "object") {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
726
742
|
|
|
727
743
|
if (typeof node.innerHTML === "string" && isTemplateString(node.innerHTML)) {
|
|
728
744
|
const resolved = evaluateStaticTemplate(node.innerHTML, scope);
|
|
729
745
|
if (resolved != null) {
|
|
730
746
|
// Encode any remaining `${` as HTML entities so the compile phase won't
|
|
731
|
-
//
|
|
732
|
-
//
|
|
747
|
+
// Re-interpret them as template expressions. After resolution, any `${` in the
|
|
748
|
+
// Result is literal content (e.g., code examples), not an intentional template.
|
|
733
749
|
node.innerHTML = String(resolved).replaceAll("${", "${");
|
|
734
750
|
}
|
|
735
751
|
}
|
|
@@ -758,32 +774,26 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
|
|
|
758
774
|
}
|
|
759
775
|
}
|
|
760
776
|
}
|
|
761
|
-
const rawChildren = node.children
|
|
762
|
-
if (
|
|
763
|
-
rawChildren
|
|
764
|
-
typeof rawChildren === "object" &&
|
|
765
|
-
!Array.isArray(rawChildren) &&
|
|
766
|
-
(rawChildren as JxMutableNode).$prototype === "Array"
|
|
767
|
-
) {
|
|
768
|
-
const arrayDef = rawChildren as JxMutableNode;
|
|
769
|
-
const itemsSrc = arrayDef.items;
|
|
777
|
+
const rawChildren = node.children;
|
|
778
|
+
if (isMappedArray(rawChildren)) {
|
|
779
|
+
const itemsSrc = rawChildren.items;
|
|
770
780
|
let items = null;
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
items = resolveRefValue(ref, scope);
|
|
781
|
+
if (isRef(itemsSrc)) {
|
|
782
|
+
items = resolveRefValue(itemsSrc.$ref, scope);
|
|
774
783
|
} else if (Array.isArray(itemsSrc)) {
|
|
775
784
|
items = itemsSrc;
|
|
776
785
|
}
|
|
777
|
-
|
|
786
|
+
const mapTemplate = rawChildren.map;
|
|
787
|
+
if (Array.isArray(items) && mapTemplate) {
|
|
778
788
|
node.children = items.map((item, index) => {
|
|
779
789
|
const childScope = Object.create(scope);
|
|
780
|
-
childScope.$map = {
|
|
790
|
+
childScope.$map = { index, item };
|
|
781
791
|
childScope["$map/item"] = item;
|
|
782
792
|
childScope["$map/index"] = index;
|
|
783
|
-
const expanded = expandMapTemplate(
|
|
784
|
-
resolveDocTemplates(expanded
|
|
793
|
+
const expanded = expandMapTemplate(mapTemplate, childScope);
|
|
794
|
+
resolveDocTemplates(expanded, childScope);
|
|
785
795
|
return expanded;
|
|
786
|
-
})
|
|
796
|
+
});
|
|
787
797
|
return;
|
|
788
798
|
}
|
|
789
799
|
}
|
|
@@ -813,29 +823,46 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
|
|
|
813
823
|
}
|
|
814
824
|
}
|
|
815
825
|
resolveDocTemplates(child, scope);
|
|
816
|
-
i
|
|
826
|
+
i += 1;
|
|
817
827
|
}
|
|
818
828
|
}
|
|
819
829
|
}
|
|
820
830
|
|
|
831
|
+
/** Collector for CSS rules extracted from component slot content during expansion. */
|
|
832
|
+
interface SlotCssCollector {
|
|
833
|
+
rules: string[];
|
|
834
|
+
counter: { n: number };
|
|
835
|
+
media: Record<string, string>;
|
|
836
|
+
}
|
|
837
|
+
|
|
821
838
|
/**
|
|
822
839
|
* Walk the document tree and expand registered custom elements in-place. Applies $props via
|
|
823
|
-
* preRenderComponentHtml, marks static/prerendered.
|
|
840
|
+
* preRenderComponentHtml, marks static/prerendered. Slot children get their static styles collected
|
|
841
|
+
* into `slotCss` (assigning jxs-N classes) before being serialized to HTML.
|
|
824
842
|
*
|
|
825
843
|
* @param {JxElement | string} node
|
|
826
844
|
* @param {Map<string, JxElement>} componentDefs
|
|
845
|
+
* @param {SlotCssCollector} [slotCss]
|
|
827
846
|
*/
|
|
828
|
-
function expandComponents(
|
|
829
|
-
|
|
847
|
+
function expandComponents(
|
|
848
|
+
node: JxElement | string,
|
|
849
|
+
componentDefs: Map<string, JxElement>,
|
|
850
|
+
slotCss?: SlotCssCollector,
|
|
851
|
+
) {
|
|
852
|
+
if (!node || typeof node !== "object") {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
830
855
|
if (Array.isArray(node)) {
|
|
831
|
-
|
|
856
|
+
for (const n of node) {
|
|
857
|
+
expandComponents(n, componentDefs, slotCss);
|
|
858
|
+
}
|
|
832
859
|
return;
|
|
833
860
|
}
|
|
834
861
|
|
|
835
862
|
// Recurse into children first (bottom-up expansion)
|
|
836
863
|
if (Array.isArray(node.children)) {
|
|
837
864
|
for (const child of node.children) {
|
|
838
|
-
expandComponents(child, componentDefs);
|
|
865
|
+
expandComponents(child, componentDefs, slotCss);
|
|
839
866
|
}
|
|
840
867
|
}
|
|
841
868
|
|
|
@@ -843,7 +870,14 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
|
|
|
843
870
|
if (def) {
|
|
844
871
|
const slotContent =
|
|
845
872
|
Array.isArray(node.children) && node.children.length > 0
|
|
846
|
-
? node.children
|
|
873
|
+
? node.children
|
|
874
|
+
.map((c: JxElement | string) => {
|
|
875
|
+
if (slotCss && c && typeof c === "object") {
|
|
876
|
+
collectStyles(c, slotCss.rules, slotCss.media, "", slotCss.counter, "jxs");
|
|
877
|
+
}
|
|
878
|
+
return renderStaticNode(c, {}, null);
|
|
879
|
+
})
|
|
880
|
+
.join("\n")
|
|
847
881
|
: null;
|
|
848
882
|
|
|
849
883
|
const innerHTML = preRenderComponentHtml(def, node.$props || null, slotContent);
|
|
@@ -854,17 +888,19 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
|
|
|
854
888
|
|
|
855
889
|
// Resolve template-string host styles with props (per-instance values like background-image)
|
|
856
890
|
if (def.style && node.$props) {
|
|
857
|
-
|
|
891
|
+
const stateDefs: Record<string, JxStateDefinition> = { ...def.state };
|
|
858
892
|
for (const [key, value] of Object.entries(node.$props)) {
|
|
859
|
-
|
|
860
|
-
|
|
893
|
+
stateDefs[key] =
|
|
894
|
+
key in stateDefs ? (value as JxStateDefinition) : (value as JxStateDefinition);
|
|
861
895
|
}
|
|
862
896
|
const scope = buildInitialScope(stateDefs, null);
|
|
863
897
|
const resolvedStyle: Record<string, unknown> = {};
|
|
864
898
|
for (const [prop, value] of Object.entries(def.style)) {
|
|
865
899
|
if (typeof value === "string" && isTemplateString(value)) {
|
|
866
900
|
const resolved = resolveStaticValue(value, scope);
|
|
867
|
-
if (resolved != null)
|
|
901
|
+
if (resolved != null) {
|
|
902
|
+
resolvedStyle[prop] = resolved;
|
|
903
|
+
}
|
|
868
904
|
}
|
|
869
905
|
}
|
|
870
906
|
if (Object.keys(resolvedStyle).length > 0) {
|
|
@@ -874,8 +910,11 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
|
|
|
874
910
|
|
|
875
911
|
delete node.$props;
|
|
876
912
|
|
|
877
|
-
if (isStatic)
|
|
878
|
-
|
|
913
|
+
if (isStatic) {
|
|
914
|
+
node.$static = true;
|
|
915
|
+
} else {
|
|
916
|
+
node.$prerendered = true;
|
|
917
|
+
}
|
|
879
918
|
}
|
|
880
919
|
}
|
|
881
920
|
|
|
@@ -892,27 +931,32 @@ function expandComponents(node: JxElement | string, componentDefs: Map<string, J
|
|
|
892
931
|
function injectComponentScripts(
|
|
893
932
|
html: string,
|
|
894
933
|
allComponentTags: string[],
|
|
895
|
-
cssMap
|
|
896
|
-
staticTags
|
|
934
|
+
cssMap = new Map<string, string>(),
|
|
935
|
+
staticTags = new Set<string>(),
|
|
897
936
|
) {
|
|
898
937
|
// Find which components are actually referenced in this page
|
|
899
938
|
const usedTags = allComponentTags.filter(
|
|
900
|
-
(tag: string) => html.includes(`<${tag}`), //
|
|
939
|
+
(tag: string) => html.includes(`<${tag}`), // Matches <tag> and <tag ...>
|
|
901
940
|
);
|
|
902
|
-
if (usedTags.length === 0)
|
|
941
|
+
if (usedTags.length === 0) {
|
|
942
|
+
return html;
|
|
943
|
+
}
|
|
903
944
|
|
|
904
945
|
// Inject CSS links in <head> for ALL components that have CSS sidecars
|
|
905
946
|
const cssLinks = usedTags
|
|
906
947
|
.filter((tag: string) => cssMap.has(tag))
|
|
907
948
|
.map((tag: string) => `<link rel="stylesheet" href="/components/${tag}.css">`)
|
|
908
949
|
.join("\n ");
|
|
950
|
+
let result = html;
|
|
909
951
|
if (cssLinks) {
|
|
910
|
-
|
|
952
|
+
result = result.replace("</head>", ` ${cssLinks}\n</head>`);
|
|
911
953
|
}
|
|
912
954
|
|
|
913
955
|
// Only inject JS for components that have non-static instances
|
|
914
956
|
const jsTags = usedTags.filter((tag: string) => !staticTags.has(tag));
|
|
915
|
-
if (jsTags.length === 0)
|
|
957
|
+
if (jsTags.length === 0) {
|
|
958
|
+
return result;
|
|
959
|
+
}
|
|
916
960
|
|
|
917
961
|
// Build import map (needed for @vue/reactivity and lit-html)
|
|
918
962
|
const importMap = `<script type="importmap">
|
|
@@ -929,10 +973,10 @@ function injectComponentScripts(
|
|
|
929
973
|
.join("\n ");
|
|
930
974
|
|
|
931
975
|
// Check if an import map already exists (from islands etc.)
|
|
932
|
-
const hasImportMap =
|
|
976
|
+
const hasImportMap = result.includes('<script type="importmap">');
|
|
933
977
|
const injection = (hasImportMap ? "" : `${importMap}\n `) + moduleScripts;
|
|
934
978
|
|
|
935
|
-
return
|
|
979
|
+
return result.replace("</body>", ` ${injection}\n</body>`);
|
|
936
980
|
}
|
|
937
981
|
|
|
938
982
|
/**
|
|
@@ -969,23 +1013,28 @@ function injectHead(html: string, headEntries: JxHeadEntry[], lang: string) {
|
|
|
969
1013
|
let preservedBlocks = "";
|
|
970
1014
|
if (existingMatch) {
|
|
971
1015
|
const styles = existingMatch[1].match(/<style>[\s\S]*?<\/style>/gi);
|
|
972
|
-
if (styles)
|
|
1016
|
+
if (styles) {
|
|
1017
|
+
preservedBlocks += `\n ${styles.join("\n ")}`;
|
|
1018
|
+
}
|
|
973
1019
|
const scripts = existingMatch[1].match(/<script[\s\S]*?<\/script>/gi);
|
|
974
|
-
if (scripts)
|
|
1020
|
+
if (scripts) {
|
|
1021
|
+
preservedBlocks += `\n ${scripts.join("\n ")}`;
|
|
1022
|
+
}
|
|
975
1023
|
}
|
|
976
|
-
|
|
977
|
-
|
|
1024
|
+
let result = html;
|
|
1025
|
+
if (headPattern.test(result)) {
|
|
1026
|
+
result = result.replace(headPattern, `<head>\n ${headHtml}${preservedBlocks}\n</head>`);
|
|
978
1027
|
}
|
|
979
1028
|
|
|
980
1029
|
// Set the lang attribute on <html>
|
|
981
|
-
|
|
1030
|
+
result = result.replace(/<html\s[^>]*>/i, (match: string) => {
|
|
982
1031
|
if (/lang=/.test(match)) {
|
|
983
1032
|
return match.replace(/lang="[^"]*"/, `lang="${lang}"`);
|
|
984
1033
|
}
|
|
985
1034
|
return match.replace("<html", `<html lang="${lang}"`);
|
|
986
1035
|
});
|
|
987
1036
|
|
|
988
|
-
return
|
|
1037
|
+
return result;
|
|
989
1038
|
}
|
|
990
1039
|
|
|
991
1040
|
/**
|
|
@@ -1011,7 +1060,7 @@ function routeToOutputPath(urlPattern: string, outDir: string, trailingSlash: st
|
|
|
1011
1060
|
return join(outDir, segments, "index.html");
|
|
1012
1061
|
}
|
|
1013
1062
|
|
|
1014
|
-
//
|
|
1063
|
+
// TrailingSlash: "never" or default
|
|
1015
1064
|
return join(outDir, `${segments}.html`);
|
|
1016
1065
|
}
|
|
1017
1066
|
|
|
@@ -1055,14 +1104,14 @@ function generateRedirects(
|
|
|
1055
1104
|
</html>`;
|
|
1056
1105
|
mkdirSync(dirname(htmlPath), { recursive: true });
|
|
1057
1106
|
writeFileSync(htmlPath, html, "utf8");
|
|
1058
|
-
count
|
|
1107
|
+
count += 1;
|
|
1059
1108
|
redirectLines.push(`${source} ${dest} ${status}`);
|
|
1060
1109
|
}
|
|
1061
1110
|
|
|
1062
1111
|
// Write _redirects file (Netlify/Cloudflare format)
|
|
1063
1112
|
if (redirectLines.length > 0) {
|
|
1064
|
-
writeFileSync(join(outDir, "_redirects"), redirectLines.join("\n")
|
|
1065
|
-
count
|
|
1113
|
+
writeFileSync(join(outDir, "_redirects"), `${redirectLines.join("\n")}\n`, "utf8");
|
|
1114
|
+
count += 1;
|
|
1066
1115
|
}
|
|
1067
1116
|
|
|
1068
1117
|
return count;
|
|
@@ -1073,7 +1122,7 @@ function generateRedirects(
|
|
|
1073
1122
|
* @returns {string}
|
|
1074
1123
|
*/
|
|
1075
1124
|
function escapeHtml(str: string) {
|
|
1076
|
-
return String(str).
|
|
1125
|
+
return String(str).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
1077
1126
|
}
|
|
1078
1127
|
|
|
1079
1128
|
/**
|
|
@@ -1081,5 +1130,5 @@ function escapeHtml(str: string) {
|
|
|
1081
1130
|
* @returns {string}
|
|
1082
1131
|
*/
|
|
1083
1132
|
function escapeAttr(str: string) {
|
|
1084
|
-
return String(str).
|
|
1133
|
+
return String(str).replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
1085
1134
|
}
|