@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
|
@@ -7,41 +7,58 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { basename, extname, resolve } from "node:path";
|
|
11
11
|
import {
|
|
12
|
-
processImage,
|
|
13
12
|
buildSrcset,
|
|
14
|
-
contentHash,
|
|
15
13
|
configHash,
|
|
14
|
+
contentHash,
|
|
16
15
|
getImageMetadata,
|
|
16
|
+
processImage,
|
|
17
17
|
} from "./image-optimizer.ts";
|
|
18
|
-
import { getCached,
|
|
18
|
+
import { getCached, getImageCacheDir, setCached } from "./image-cache.ts";
|
|
19
19
|
|
|
20
|
-
import type { ImageConfig } from "./image-optimizer.ts";
|
|
21
|
-
import type {
|
|
22
|
-
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
20
|
+
import type { ImageConfig, ImageManifest } from "./image-optimizer.ts";
|
|
21
|
+
import type { JxDocument, JxMutableNode } from "@jxsuite/schema/types";
|
|
23
22
|
import type { CacheManifest } from "./image-cache.ts";
|
|
24
|
-
import type { JxDocument } from "@jxsuite/schema/types";
|
|
25
23
|
|
|
26
24
|
const SKIP_EXTENSIONS = new Set([".svg", ".gif"]);
|
|
27
25
|
const EXTERNAL_PREFIXES = ["http://", "https://", "data:", "//"];
|
|
28
26
|
|
|
29
27
|
/** Per-build memo of original image dimensions + content hash (cloudflare mode). */
|
|
30
|
-
export type ImageMetaCache = Map<
|
|
28
|
+
export type ImageMetaCache = Map<
|
|
29
|
+
string,
|
|
30
|
+
{ width: number | null; height: number | null; hash: string }
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
let sharpWarned = false;
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* Resolve original image dimensions and content hash for cloudflare mode. Reads only the image
|
|
34
|
-
* header via Sharp — no variants are generated.
|
|
37
|
+
* header via Sharp — no variants are generated. Dimensions degrade to null when Sharp is
|
|
38
|
+
* unavailable (e.g. native binary won't load): srcsets are still emitted (fit=scale-down guards
|
|
39
|
+
* against upscaling), only the width/height attributes are skipped.
|
|
35
40
|
*
|
|
36
41
|
* @param {string} absoluteSrc
|
|
37
42
|
* @param {ImageMetaCache} metaCache
|
|
38
|
-
* @returns {Promise<{ width: number; height: number; hash: string }>}
|
|
43
|
+
* @returns {Promise<{ width: number | null; height: number | null; hash: string }>}
|
|
39
44
|
*/
|
|
40
45
|
async function resolveCfMeta(absoluteSrc: string, metaCache: ImageMetaCache) {
|
|
41
46
|
let meta = metaCache.get(absoluteSrc);
|
|
42
47
|
if (!meta) {
|
|
43
|
-
const
|
|
44
|
-
|
|
48
|
+
const hash = contentHash(absoluteSrc);
|
|
49
|
+
try {
|
|
50
|
+
const m = await getImageMetadata(absoluteSrc);
|
|
51
|
+
meta = { width: m.width, height: m.height, hash };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!sharpWarned) {
|
|
54
|
+
sharpWarned = true;
|
|
55
|
+
console.warn(
|
|
56
|
+
`Could not read image dimensions (${(error as Error).message}) — ` +
|
|
57
|
+
`emitting srcsets without width/height attributes.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
meta = { width: null, height: null, hash };
|
|
61
|
+
}
|
|
45
62
|
metaCache.set(absoluteSrc, meta);
|
|
46
63
|
}
|
|
47
64
|
return meta;
|
|
@@ -71,7 +88,7 @@ function buildCloudflareSrcset(
|
|
|
71
88
|
const suffix = hash ? `?v=${hash}` : "";
|
|
72
89
|
return config.widths
|
|
73
90
|
.filter((w) => w <= originalWidth)
|
|
74
|
-
.
|
|
91
|
+
.toSorted((a, b) => a - b)
|
|
75
92
|
.map(
|
|
76
93
|
(w) =>
|
|
77
94
|
`/cdn-cgi/image/width=${w},quality=${quality},fit=scale-down,format=auto` +
|
|
@@ -100,10 +117,14 @@ async function resolveManifest(
|
|
|
100
117
|
|
|
101
118
|
if (cached) {
|
|
102
119
|
const allExist = cached.variants.every((v) => existsSync(v.absolutePath));
|
|
103
|
-
if (allExist)
|
|
120
|
+
if (allExist) {
|
|
121
|
+
return cached;
|
|
122
|
+
}
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
if (!cached)
|
|
125
|
+
if (!cached) {
|
|
126
|
+
console.log(` Optimizing ${basename(absoluteSrc)}...`);
|
|
127
|
+
}
|
|
107
128
|
const manifest = await processImage(absoluteSrc, getImageCacheDir(projectRoot), config);
|
|
108
129
|
setCached(cache, key, src, manifest);
|
|
109
130
|
return manifest;
|
|
@@ -116,11 +137,21 @@ async function resolveManifest(
|
|
|
116
137
|
* @returns {boolean}
|
|
117
138
|
*/
|
|
118
139
|
function shouldSkip(src: string) {
|
|
119
|
-
if (typeof src !== "string")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
140
|
+
if (typeof src !== "string") {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (!src) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (src.includes("${")) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (EXTERNAL_PREFIXES.some((p) => src.startsWith(p))) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (SKIP_EXTENSIONS.has(extname(src).toLowerCase())) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
124
155
|
return false;
|
|
125
156
|
}
|
|
126
157
|
|
|
@@ -136,13 +167,23 @@ function shouldSkip(src: string) {
|
|
|
136
167
|
* @returns {boolean}
|
|
137
168
|
*/
|
|
138
169
|
function isAllowedRemote(src: string, config: ImageConfig) {
|
|
139
|
-
if (config.service !== "cloudflare")
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
170
|
+
if (config.service !== "cloudflare") {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (!config.remoteDomains?.length) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (typeof src !== "string" || !src.startsWith("https://")) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (src.includes("${")) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
143
182
|
try {
|
|
144
183
|
const url = new URL(src);
|
|
145
|
-
if (SKIP_EXTENSIONS.has(extname(url.pathname).toLowerCase()))
|
|
184
|
+
if (SKIP_EXTENSIONS.has(extname(url.pathname).toLowerCase())) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
146
187
|
return config.remoteDomains.includes(url.hostname);
|
|
147
188
|
} catch {
|
|
148
189
|
return false;
|
|
@@ -186,9 +227,11 @@ export async function transformImageNodes(
|
|
|
186
227
|
cache: CacheManifest | null,
|
|
187
228
|
metaCache?: ImageMetaCache,
|
|
188
229
|
) {
|
|
189
|
-
const imageRefs
|
|
230
|
+
const imageRefs = new Map<string, ImageManifest>();
|
|
190
231
|
|
|
191
|
-
if (!config.optimize)
|
|
232
|
+
if (!config.optimize) {
|
|
233
|
+
return { imageRefs };
|
|
234
|
+
}
|
|
192
235
|
|
|
193
236
|
const meta = metaCache ?? new Map();
|
|
194
237
|
await walkAndTransform(doc, config, projectRoot, cache, meta, imageRefs);
|
|
@@ -212,7 +255,9 @@ async function walkAndTransform(
|
|
|
212
255
|
metaCache: ImageMetaCache,
|
|
213
256
|
imageRefs: Map<string, ImageManifest>,
|
|
214
257
|
) {
|
|
215
|
-
if (!node || typeof node !== "object")
|
|
258
|
+
if (!node || typeof node !== "object") {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
216
261
|
|
|
217
262
|
if (node.tagName === "img") {
|
|
218
263
|
await transformImgNode(node, config, projectRoot, cache, metaCache, imageRefs);
|
|
@@ -231,7 +276,9 @@ async function walkAndTransform(
|
|
|
231
276
|
|
|
232
277
|
if (Array.isArray(node.children)) {
|
|
233
278
|
for (const child of node.children) {
|
|
234
|
-
if (typeof child === "string")
|
|
279
|
+
if (typeof child === "string") {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
235
282
|
await walkAndTransform(child, config, projectRoot, cache, metaCache, imageRefs);
|
|
236
283
|
}
|
|
237
284
|
}
|
|
@@ -253,43 +300,59 @@ async function transformImgNode(
|
|
|
253
300
|
metaCache: ImageMetaCache,
|
|
254
301
|
imageRefs: Map<string, ImageManifest>,
|
|
255
302
|
) {
|
|
256
|
-
if (!node.attributes)
|
|
303
|
+
if (!node.attributes) {
|
|
304
|
+
node.attributes = {};
|
|
305
|
+
}
|
|
257
306
|
|
|
258
307
|
const src = node.attributes.src ?? node.src;
|
|
308
|
+
// Bound ($ref) or missing srcs cannot be optimized at build time.
|
|
309
|
+
if (typeof src !== "string") {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
259
312
|
const remote = isAllowedRemote(src, config);
|
|
260
|
-
if (!remote && shouldSkip(src))
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
313
|
+
if (!remote && shouldSkip(src)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (node.attributes["data-no-optimize"] !== undefined) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
265
319
|
|
|
266
320
|
let srcset;
|
|
267
321
|
let original: { width: number; height: number } | undefined;
|
|
268
322
|
|
|
269
323
|
if (remote) {
|
|
270
324
|
// Original dimensions are unknown without fetching — emit every configured width and let
|
|
271
|
-
//
|
|
325
|
+
// Fit=scale-down avoid upscaling past the source size.
|
|
272
326
|
srcset = buildCloudflareSrcset(src, config, Infinity, null);
|
|
273
|
-
} else if (config.service === "cloudflare") {
|
|
274
|
-
const meta = await resolveCfMeta(absoluteSrc as string, metaCache);
|
|
275
|
-
srcset = buildCloudflareSrcset(src, config, meta.width, meta.hash);
|
|
276
|
-
original = meta;
|
|
277
327
|
} else {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
manifest = await resolveManifest(absoluteSrc as string, src, config, projectRoot, cache!);
|
|
282
|
-
imageRefs.set(absoluteSrc as string, manifest);
|
|
328
|
+
const absoluteSrc = resolveImagePath(src, projectRoot);
|
|
329
|
+
if (!existsSync(absoluteSrc)) {
|
|
330
|
+
return;
|
|
283
331
|
}
|
|
284
332
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
333
|
+
if (config.service === "cloudflare") {
|
|
334
|
+
const meta = await resolveCfMeta(absoluteSrc, metaCache);
|
|
335
|
+
srcset = buildCloudflareSrcset(src, config, meta.width ?? Infinity, meta.hash);
|
|
336
|
+
if (meta.width != null && meta.height != null) {
|
|
337
|
+
original = { width: meta.width, height: meta.height };
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
let manifest = imageRefs.get(absoluteSrc);
|
|
341
|
+
|
|
342
|
+
if (!manifest) {
|
|
343
|
+
manifest = await resolveManifest(absoluteSrc, src, config, projectRoot, cache!);
|
|
344
|
+
imageRefs.set(absoluteSrc, manifest);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0];
|
|
348
|
+
srcset = buildSrcset(manifest.variants, preferredFormat);
|
|
349
|
+
({ original } = manifest);
|
|
350
|
+
}
|
|
288
351
|
}
|
|
289
352
|
|
|
290
353
|
if (srcset) {
|
|
291
354
|
node.attributes.srcset = srcset;
|
|
292
|
-
node.attributes.sizes
|
|
355
|
+
node.attributes.sizes ??= config.sizes;
|
|
293
356
|
}
|
|
294
357
|
|
|
295
358
|
if (!node.attributes.width && original?.width) {
|
|
@@ -333,22 +396,32 @@ async function transformInnerHtmlImages(
|
|
|
333
396
|
const replacements = [];
|
|
334
397
|
|
|
335
398
|
for (const m of html.matchAll(IMG_TAG_RE)) {
|
|
336
|
-
const tag = m
|
|
337
|
-
const attrs = m
|
|
399
|
+
const [tag] = m;
|
|
400
|
+
const [, attrs] = m;
|
|
338
401
|
|
|
339
|
-
if (SRCSET_ATTR_RE.test(attrs))
|
|
340
|
-
|
|
402
|
+
if (SRCSET_ATTR_RE.test(attrs)) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (DATA_NO_OPT_RE.test(attrs)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
341
408
|
|
|
342
409
|
const srcMatch = attrs.match(SRC_ATTR_RE);
|
|
343
|
-
if (!srcMatch)
|
|
410
|
+
if (!srcMatch) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
344
413
|
|
|
345
414
|
// Attribute values in pre-rendered innerHTML are entity-escaped — decode for processing
|
|
346
415
|
const src = srcMatch[1].replaceAll("&", "&");
|
|
347
416
|
const remote = isAllowedRemote(src, config);
|
|
348
|
-
if (!remote && shouldSkip(src))
|
|
417
|
+
if (!remote && shouldSkip(src)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
349
420
|
|
|
350
421
|
const absoluteSrc = remote ? null : resolveImagePath(src, projectRoot);
|
|
351
|
-
if (absoluteSrc && !existsSync(absoluteSrc))
|
|
422
|
+
if (absoluteSrc && !existsSync(absoluteSrc)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
352
425
|
|
|
353
426
|
let srcset;
|
|
354
427
|
let original: { width: number; height: number } | undefined;
|
|
@@ -357,8 +430,10 @@ async function transformInnerHtmlImages(
|
|
|
357
430
|
srcset = buildCloudflareSrcset(src, config, Infinity, null);
|
|
358
431
|
} else if (config.service === "cloudflare") {
|
|
359
432
|
const meta = await resolveCfMeta(absoluteSrc as string, metaCache);
|
|
360
|
-
srcset = buildCloudflareSrcset(src, config, meta.width, meta.hash);
|
|
361
|
-
|
|
433
|
+
srcset = buildCloudflareSrcset(src, config, meta.width ?? Infinity, meta.hash);
|
|
434
|
+
if (meta.width != null && meta.height != null) {
|
|
435
|
+
original = { width: meta.width, height: meta.height };
|
|
436
|
+
}
|
|
362
437
|
} else {
|
|
363
438
|
let manifest = imageRefs.get(absoluteSrc as string);
|
|
364
439
|
if (!manifest) {
|
|
@@ -368,9 +443,11 @@ async function transformInnerHtmlImages(
|
|
|
368
443
|
|
|
369
444
|
const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0];
|
|
370
445
|
srcset = buildSrcset(manifest.variants, preferredFormat);
|
|
371
|
-
original = manifest
|
|
446
|
+
({ original } = manifest);
|
|
447
|
+
}
|
|
448
|
+
if (!srcset) {
|
|
449
|
+
continue;
|
|
372
450
|
}
|
|
373
|
-
if (!srcset) continue;
|
|
374
451
|
|
|
375
452
|
let extra = ` srcset="${srcset}" sizes="${config.sizes}"`;
|
|
376
453
|
if (!/\bwidth=/.test(attrs) && original?.width) {
|
|
@@ -380,8 +457,12 @@ async function transformInnerHtmlImages(
|
|
|
380
457
|
extra += ` height="${original.height}"`;
|
|
381
458
|
}
|
|
382
459
|
if (config.lazyLoad && !/\bloading="eager"/.test(attrs)) {
|
|
383
|
-
if (!/\bloading=/.test(attrs))
|
|
384
|
-
|
|
460
|
+
if (!/\bloading=/.test(attrs)) {
|
|
461
|
+
extra += ` loading="lazy"`;
|
|
462
|
+
}
|
|
463
|
+
if (!/\bdecoding=/.test(attrs)) {
|
|
464
|
+
extra += ` decoding="async"`;
|
|
465
|
+
}
|
|
385
466
|
}
|
|
386
467
|
|
|
387
468
|
replacements.push({ match: tag, replacement: `<img${attrs}${extra}>` });
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* - Named slots use attributes.slot on page children
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { parseJxDocument } from "@jxsuite/schema/parse";
|
|
17
18
|
import { resolve } from "node:path";
|
|
18
|
-
import type {
|
|
19
|
+
import type { JxDocument, JxElement } from "@jxsuite/schema/types";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Resolve a page's layout, wrapping the page content in the layout structure.
|
|
@@ -48,10 +49,10 @@ export function resolveLayout(
|
|
|
48
49
|
/** @type {JxDocument} */
|
|
49
50
|
let layoutDoc;
|
|
50
51
|
try {
|
|
51
|
-
layoutDoc =
|
|
52
|
-
} catch (
|
|
53
|
-
const err =
|
|
54
|
-
throw new Error(`Invalid layout JSON at ${layoutPath}: ${err.message}
|
|
52
|
+
layoutDoc = parseJxDocument(readFileSync(layoutPath, "utf8"), layoutPath);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const err = error as Error;
|
|
55
|
+
throw new Error(`Invalid layout JSON at ${layoutPath}: ${err.message}`, { cause: error });
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Check for nested layouts (layout inheriting from another layout)
|
|
@@ -93,8 +94,12 @@ export function resolveLayout(
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
// Preserve page-level metadata
|
|
96
|
-
if (pageDoc.$head)
|
|
97
|
-
|
|
97
|
+
if (pageDoc.$head) {
|
|
98
|
+
merged._pageHead = pageDoc.$head;
|
|
99
|
+
}
|
|
100
|
+
if (pageDoc.title) {
|
|
101
|
+
merged._pageTitle = pageDoc.title;
|
|
102
|
+
}
|
|
98
103
|
|
|
99
104
|
// Remove $layout from merged doc (already resolved)
|
|
100
105
|
delete merged.$layout;
|
|
@@ -117,17 +122,23 @@ export function resolveLayout(
|
|
|
117
122
|
* @param {(JxElement | string)[]} children - Page children to distribute
|
|
118
123
|
*/
|
|
119
124
|
function distributeSlots(node: JxElement, children: (JxElement | string)[]) {
|
|
120
|
-
if (!node || typeof node !== "object")
|
|
121
|
-
|
|
125
|
+
if (!node || typeof node !== "object") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(node.children)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
122
131
|
|
|
123
132
|
// Collect named and default children
|
|
124
|
-
const named
|
|
125
|
-
const defaults: (JxElement | string)[] = []; //
|
|
133
|
+
const named = new Map<string, (JxElement | string)[]>(); // Slot name → children[]
|
|
134
|
+
const defaults: (JxElement | string)[] = []; // Children without a slot target
|
|
126
135
|
|
|
127
136
|
for (const child of children) {
|
|
128
137
|
if (child && typeof child === "object" && child.attributes?.slot) {
|
|
129
138
|
const slotName = child.attributes.slot as string;
|
|
130
|
-
if (!named.has(slotName))
|
|
139
|
+
if (!named.has(slotName)) {
|
|
140
|
+
named.set(slotName, []);
|
|
141
|
+
}
|
|
131
142
|
(named.get(slotName) as (JxElement | string)[]).push(child);
|
|
132
143
|
} else {
|
|
133
144
|
defaults.push(child);
|
|
@@ -150,8 +161,12 @@ function fillSlots(
|
|
|
150
161
|
named: Map<string, (JxElement | string)[]>,
|
|
151
162
|
defaults: (JxElement | string)[],
|
|
152
163
|
) {
|
|
153
|
-
if (!node || typeof node !== "object")
|
|
154
|
-
|
|
164
|
+
if (!node || typeof node !== "object") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!Array.isArray(node.children)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
155
170
|
|
|
156
171
|
const newChildren: (JxElement | string)[] = [];
|
|
157
172
|
|
|
@@ -162,12 +177,12 @@ function fillSlots(
|
|
|
162
177
|
if (slotName && named.has(slotName as string)) {
|
|
163
178
|
// Named slot — replace with matching children
|
|
164
179
|
newChildren.push(...(named.get(slotName as string) as (JxElement | string)[]));
|
|
165
|
-
named.delete(slotName as string); //
|
|
180
|
+
named.delete(slotName as string); // Consumed
|
|
166
181
|
} else if (!slotName && defaults.length > 0) {
|
|
167
182
|
// Default slot — replace with unassigned children
|
|
168
183
|
newChildren.push(...defaults);
|
|
169
184
|
// Don't clear defaults — only one default slot should exist,
|
|
170
|
-
//
|
|
185
|
+
// But if there are multiple, the first one wins
|
|
171
186
|
} else {
|
|
172
187
|
// No matching content — keep slot's fallback children
|
|
173
188
|
if (Array.isArray(child.children)) {
|
|
@@ -191,5 +206,5 @@ function fillSlots(
|
|
|
191
206
|
* @returns {JxDocument}
|
|
192
207
|
*/
|
|
193
208
|
function deepClone(obj: JxDocument) {
|
|
194
|
-
return
|
|
209
|
+
return structuredClone(obj);
|
|
195
210
|
}
|