@jxsuite/compiler 0.28.4 → 0.29.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.28.4",
3
+ "version": "0.29.0",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,17 +35,18 @@
35
35
  "scripts": {
36
36
  "prepare": "bun build src/compiler.ts src/cli.ts --outdir=dist --format=esm --minify --target=node",
37
37
  "build": "bun run prepare",
38
+ "build:metafile": "bun build src/compiler.ts src/cli.ts --outdir=dist --format=esm --minify --target=node --metafile=metafile.json",
38
39
  "build:site": "bun src/cli.ts build",
39
40
  "test": "bun test --isolate",
40
41
  "test:coverage": "bun test --isolate --coverage",
41
42
  "upgrade": "bunx npm-check-updates -u && bun install"
42
43
  },
43
44
  "dependencies": {
44
- "@apidevtools/json-schema-ref-parser": "^15.3.5",
45
- "@jxsuite/parser": "^0.28.4",
46
- "@jxsuite/runtime": "^0.28.4",
47
- "@jxsuite/schema": "^0.28.4",
48
- "sharp": "^0.35.0"
45
+ "@apidevtools/json-schema-ref-parser": "^15.3.6",
46
+ "@jxsuite/parser": "^0.29.0",
47
+ "@jxsuite/runtime": "^0.29.0",
48
+ "@jxsuite/schema": "^0.29.0",
49
+ "sharp": "^0.35.1"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@happy-dom/global-registrator": "^20.10.2",
package/src/compiler.ts CHANGED
@@ -22,13 +22,9 @@ import {
22
22
  isDynamic,
23
23
  tagNameToClassName,
24
24
  } from "./shared.ts";
25
- import { compileServer, compileSiteServer } from "./targets/compile-server.ts";
25
+ import { compileServer } from "./targets/compile-server.ts";
26
26
 
27
- import {
28
- compileElement,
29
- compileElementPage,
30
- emitElementModule,
31
- } from "./targets/compile-element.ts";
27
+ import { emitElementModule } from "./targets/compile-element.ts";
32
28
  import { compileStaticPage } from "./targets/compile-static.ts";
33
29
  import { compileClient } from "./targets/compile-client.ts";
34
30
  import { isClassDef } from "@jxsuite/schema/guards";
@@ -37,14 +33,10 @@ import type { JxDocument, JxStyle } from "@jxsuite/schema/types";
37
33
  import type { FormatRegistry } from "@jxsuite/schema/format-registry";
38
34
 
39
35
  // Re-exports for consumers
40
- export {
41
- isDynamic,
42
- compileServer,
43
- compileSiteServer,
44
- compileElement,
45
- compileElementPage,
46
- compileClient,
47
- };
36
+ export { isDynamic } from "./shared.ts";
37
+ export { compileServer, compileSiteServer } from "./targets/compile-server.ts";
38
+ export { compileElement, compileElementPage } from "./targets/compile-element.ts";
39
+ export { compileClient } from "./targets/compile-client.ts";
48
40
 
49
41
  // ─── Entry ────────────────────────────────────────────────────────────────────
50
42
 
package/src/shared.ts CHANGED
@@ -7,10 +7,12 @@
7
7
  */
8
8
 
9
9
  import { RESERVED_KEYS, camelToKebab, toCSSText } from "@jxsuite/runtime";
10
- import { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
10
+ import { evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
11
11
  import {
12
+ childrenContainArray,
12
13
  isExpressionDef,
13
14
  isFunctionDef,
15
+ isMappedArray,
14
16
  isPrototypeDef,
15
17
  isRef,
16
18
  isSchemaOnlyDef as isSchemaOnly,
@@ -22,7 +24,6 @@ import type { ExpressionNode } from "@jxsuite/runtime/expression";
22
24
  import type {
23
25
  JsonValue,
24
26
  JxElement,
25
- JxMappedArray,
26
27
  JxMutableNode,
27
28
  JxPrototypeDef,
28
29
  JxRef,
@@ -32,15 +33,9 @@ import type {
32
33
  } from "@jxsuite/schema/types";
33
34
 
34
35
  // Re-export runtime utilities used by submodules
35
- export {
36
- camelToKebab,
37
- toCSSText,
38
- RESERVED_KEYS,
39
- compileExpression,
40
- isMutating,
41
- evaluateExpression,
42
- };
43
- export type { ExpressionNode };
36
+ export { RESERVED_KEYS, camelToKebab, toCSSText } from "@jxsuite/runtime";
37
+ export { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
38
+ export type { ExpressionNode } from "@jxsuite/runtime/expression";
44
39
 
45
40
  // CDN defaults
46
41
  export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.32";
@@ -110,10 +105,7 @@ export function isDynamic(def: JxElement | JxMutableNode | string) {
110
105
  if (def.$switch) {
111
106
  return true;
112
107
  }
113
- if (
114
- !Array.isArray(def.children) &&
115
- (def.children as JxMappedArray | undefined)?.$prototype === "Array"
116
- ) {
108
+ if (isMappedArray(def) || childrenContainArray(def.children)) {
117
109
  return true;
118
110
  }
119
111
 
@@ -170,10 +162,7 @@ export function isNodeDynamic(def: JxElement | JxMutableNode | string) {
170
162
  if (def.$switch) {
171
163
  return true;
172
164
  }
173
- if (
174
- !Array.isArray(def.children) &&
175
- (def.children as JxMappedArray | undefined)?.$prototype === "Array"
176
- ) {
165
+ if (isMappedArray(def) || childrenContainArray(def.children)) {
177
166
  return true;
178
167
  }
179
168
 
@@ -252,9 +241,9 @@ export function createCompileContext(
252
241
  scopeDefs: Record<string, unknown> = {},
253
242
  media: Record<string, string> = {},
254
243
  ): CompileContext {
255
- const scope = raw?.state
244
+ const scope: Record<string, unknown> = raw?.state
256
245
  ? buildInitialScope(raw.state, parentScope)
257
- : (parentScope ?? Object.create(null));
246
+ : (parentScope ?? (Object.create(null) as Record<string, unknown>));
258
247
  return { media, scope, scopeDefs };
259
248
  }
260
249
 
@@ -267,7 +256,7 @@ export function buildInitialScope(
267
256
  defs: Record<string, JxStateDefinition> = {},
268
257
  parentScope: Record<string, unknown> | null = null,
269
258
  ) {
270
- const scope = Object.create(parentScope ?? null);
259
+ const scope = Object.create(parentScope ?? null) as Record<string, unknown>;
271
260
 
272
261
  for (const [key, def] of Object.entries(defs)) {
273
262
  if (typeof def !== "object" || def === null || Array.isArray(def)) {
@@ -306,7 +295,9 @@ export function buildInitialScope(
306
295
  if (isFunctionDef(def)) {
307
296
  if (def.body) {
308
297
  const names = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
309
- const fn = new Function("state", ...names, def.body);
298
+ const fn = new Function("state", ...names, def.body) as (
299
+ state: Record<string, unknown>,
300
+ ) => unknown;
310
301
  if (def.body.includes("return")) {
311
302
  defineLazyScopeValue(scope, key, () => fn(scope));
312
303
  } else {
@@ -393,7 +384,7 @@ export function resolveRefValue(refValue: unknown, scope: Record<string, unknown
393
384
  }
394
385
  if (refValue.startsWith("$map/")) {
395
386
  const parts = refValue.split("/");
396
- const [, key] = parts;
387
+ const key = parts[1]!;
397
388
  const base = (scope.$map as Record<string, unknown> | undefined)?.[key] ?? scope[`$map/${key}`];
398
389
  return parts.length > 2 ? getPathValue(base, parts.slice(2).join("/")) : base;
399
390
  }
@@ -417,10 +408,16 @@ export function evaluateStaticTemplate(str: string, scope: Record<string, unknow
417
408
  try {
418
409
  const singleExprMatch = str.match(/^\$\{(.+)\}$/s);
419
410
  if (singleExprMatch) {
420
- const fn = new Function("state", "$map", `return (${singleExprMatch[1]})`);
411
+ const fn = new Function("state", "$map", `return (${singleExprMatch[1]})`) as (
412
+ state: Record<string, unknown>,
413
+ $map: unknown,
414
+ ) => unknown;
421
415
  return fn(scope, scope?.$map);
422
416
  }
423
- const fn = new Function("state", "$map", `return \`${str}\``);
417
+ const fn = new Function("state", "$map", `return \`${str}\``) as (
418
+ state: Record<string, unknown>,
419
+ $map: unknown,
420
+ ) => unknown;
424
421
  return fn(scope, scope?.$map);
425
422
  } catch {
426
423
  return null;
@@ -632,7 +629,7 @@ export function compileStyles(
632
629
  if (mProps) {
633
630
  rules.push(`${atRule} { ${selector} { ${mProps} } }`);
634
631
  }
635
- for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
632
+ for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
636
633
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
637
634
  continue;
638
635
  }
@@ -753,7 +750,7 @@ function emitNestedElement(
753
750
  if (mProps) {
754
751
  rules.push(`${atRule} { ${selector} { ${mProps} } }`);
755
752
  }
756
- for (const [sel, sub] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
753
+ for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
757
754
  if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
758
755
  continue;
759
756
  }
@@ -995,8 +992,7 @@ function _walkSrc(def: JxElement | JxMutableNode | string, srcs: Set<string>) {
995
992
  * @returns {{ key: string; exportName: string; src: string }[]}
996
993
  */
997
994
  export function collectServerEntries(doc: JxElement) {
998
- /** @type {Map<string, { key: string; exportName: string; src: string }>} */
999
- const entries = new Map();
995
+ const entries = new Map<string, { key: string; exportName: string; src: string }>();
1000
996
  _walkServerEntries(doc, entries);
1001
997
  return [...entries.values()];
1002
998
  }
@@ -21,6 +21,7 @@ import type { FormatEntry, FormatRegistry } from "@jxsuite/schema/format-registr
21
21
  import type {
22
22
  ContentTypeDef,
23
23
  ContentTypeSchema,
24
+ ContentTypeSchemaField,
24
25
  JxMutableNode,
25
26
  ProjectConfig,
26
27
  } from "@jxsuite/schema/types";
@@ -36,16 +37,16 @@ import type { ContentLoaderEntry } from "@jxsuite/parser/types";
36
37
  * @returns {ContentLoaderEntry[]} Array of ContentEntry shapes
37
38
  */
38
39
  function loadJSONEntries(filePath: string) {
39
- const raw = JSON.parse(readFileSync(filePath, "utf8"));
40
+ const raw = JSON.parse(readFileSync(filePath, "utf8")) as unknown;
40
41
  if (Array.isArray(raw)) {
41
- return raw.map((item: Record<string, unknown>, i: number) => ({
42
+ return (raw as Record<string, unknown>[]).map((item: Record<string, unknown>, i: number) => ({
42
43
  body: null,
43
44
  data: item,
44
45
  id: (item.id as string) ?? `${basename(filePath, ".json")}-${i}`,
45
46
  }));
46
47
  }
47
48
  // Single object file — filename is the id
48
- const rawObj: Record<string, unknown> = raw;
49
+ const rawObj = raw as Record<string, unknown>;
49
50
  return [
50
51
  {
51
52
  body: null,
@@ -108,7 +109,7 @@ export async function loadContentTypes(
108
109
  ) {
109
110
  const result = loadContentConfig(projectRoot, projectConfig);
110
111
  if (!result) {
111
- return new Map();
112
+ return new Map<string, ContentLoaderEntry[]>();
112
113
  }
113
114
 
114
115
  const { config } = result;
@@ -359,7 +360,8 @@ export function resolveContentTypeRefs(
359
360
  continue;
360
361
  }
361
362
 
362
- for (const [field, def] of Object.entries(schema.properties)) {
363
+ for (const [field, rawDef] of Object.entries(schema.properties)) {
364
+ const def = rawDef as ContentTypeSchemaField;
363
365
  if (!def.$ref?.startsWith("#/contentTypes/")) {
364
366
  continue;
365
367
  }
@@ -36,7 +36,7 @@ export function injectContext(
36
36
  doc: JxDocument,
37
37
  projectConfig: ProjectConfig,
38
38
  route: SiteRoute,
39
- contentTypes = new Map<string, ContentLoaderEntry[]>(),
39
+ _contentTypes = new Map<string, ContentLoaderEntry[]>(),
40
40
  projectRoot: string | null = null,
41
41
  ) {
42
42
  if (!doc.state) {
@@ -94,13 +94,13 @@ export function cacheKey(srcPath: string, config: ImageConfig) {
94
94
  * @param {string} projectRoot
95
95
  * @returns {CacheManifest}
96
96
  */
97
- export function loadCache(projectRoot: string) {
97
+ export function loadCache(projectRoot: string): CacheManifest {
98
98
  const manifestPath = resolve(getImageCacheDir(projectRoot), "manifest.json");
99
99
  if (!existsSync(manifestPath)) {
100
100
  return { entries: {}, version: 1 };
101
101
  }
102
102
  try {
103
- return JSON.parse(readFileSync(manifestPath, "utf8"));
103
+ return JSON.parse(readFileSync(manifestPath, "utf8")) as CacheManifest;
104
104
  } catch {
105
105
  return { entries: {}, version: 1 };
106
106
  }
@@ -10,7 +10,8 @@ import { errorMessage } from "@jxsuite/schema/parse";
10
10
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
11
11
  import { basename, extname, resolve } from "node:path";
12
12
  import { createRequire } from "node:module";
13
- import type { FormatEnum } from "sharp";
13
+ // Sharp 0.35 only has a default export (function merged with a types namespace).
14
+ import type SharpNS from "sharp";
14
15
 
15
16
  export interface ImageVariant {
16
17
  width: number; // Pixel width of the variant
@@ -41,8 +42,7 @@ export interface ImageConfig {
41
42
  remoteDomains?: string[];
42
43
  }
43
44
 
44
- // oxlint-disable-next-line typescript/consistent-type-imports -- sharp is an optional native dep loaded lazily; its `export =` module type is only nameable via import()
45
- type SharpModule = typeof import("sharp");
45
+ type SharpModule = typeof SharpNS;
46
46
 
47
47
  let _sharp: SharpModule | null = null;
48
48
 
@@ -175,7 +175,7 @@ export async function processImage(srcPath: string, cacheImgDir: string, config:
175
175
  const quality = config.quality[format as keyof ImageConfig["quality"]] ?? 80;
176
176
  const task = sharp(srcPath)
177
177
  .resize(width)
178
- .toFormat(format as keyof FormatEnum, { quality })
178
+ .toFormat(format as keyof SharpNS.FormatEnum, { quality })
179
179
  .toFile(absolutePath)
180
180
  .then(() => {});
181
181
 
@@ -340,7 +340,7 @@ async function transformImgNode(
340
340
  imageRefs.set(absoluteSrc, manifest);
341
341
  }
342
342
 
343
- const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0];
343
+ const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0]!;
344
344
  srcset = buildSrcset(manifest.variants, preferredFormat);
345
345
  }
346
346
  }
@@ -384,8 +384,8 @@ async function transformInnerHtmlImages(
384
384
  const replacements = [];
385
385
 
386
386
  for (const m of html.matchAll(IMG_TAG_RE)) {
387
- const [tag] = m;
388
- const [, attrs] = m;
387
+ const tag = m[0]!;
388
+ const attrs = m[1]!;
389
389
 
390
390
  if (SRCSET_ATTR_RE.test(attrs)) {
391
391
  continue;
@@ -400,7 +400,7 @@ async function transformInnerHtmlImages(
400
400
  }
401
401
 
402
402
  // Attribute values in pre-rendered innerHTML are entity-escaped — decode for processing
403
- const src = srcMatch[1].replaceAll("&amp;", "&");
403
+ const src = srcMatch[1]!.replaceAll("&amp;", "&");
404
404
  const remote = isAllowedRemote(src, config);
405
405
  if (!remote && shouldSkip(src)) {
406
406
  continue;
@@ -425,7 +425,7 @@ async function transformInnerHtmlImages(
425
425
  imageRefs.set(absoluteSrc as string, manifest);
426
426
  }
427
427
 
428
- const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0];
428
+ const preferredFormat = config.formats.includes("avif") ? "avif" : config.formats[0]!;
429
429
  srcset = buildSrcset(manifest.variants, preferredFormat);
430
430
  }
431
431
  if (!srcset) {
@@ -169,7 +169,7 @@ async function fileToRoute(relativePath: string, absolutePath: string, registry?
169
169
  // Convert [param] → :param and [...param] → *
170
170
  const urlPattern = urlPath.replaceAll(
171
171
  /\[\.\.\.(\w+)\]|\[(\w+)\]/g,
172
- (match: string, spread: string, named: string) => {
172
+ (_match: string, spread: string, named: string) => {
173
173
  if (spread) {
174
174
  isCatchAll = true;
175
175
  isDynamic = true;
@@ -319,10 +319,9 @@ function resolvePathEntries(
319
319
  // Data file ref: { "$ref": "./data/products.json", param: "id", field: "sku" }
320
320
  if ("$ref" in $paths && $paths.$ref) {
321
321
  const filePath = resolve(projectRoot, $paths.$ref);
322
- /** @type {Record<string, unknown>[]} */
323
- let data;
322
+ let data: unknown;
324
323
  try {
325
- data = JSON.parse(readFileSync(filePath, "utf8"));
324
+ data = JSON.parse(readFileSync(filePath, "utf8")) as unknown;
326
325
  } catch (error) {
327
326
  const err = error as Error;
328
327
  console.warn(`Warning: $paths.$ref could not load "${$paths.$ref}": ${err.message}`);
@@ -334,7 +333,7 @@ function resolvePathEntries(
334
333
  }
335
334
  const param = $paths.param ?? "id";
336
335
  const field = $paths.field ?? "id";
337
- return data.map((item: Record<string, unknown>) => ({
336
+ return (data as Record<string, unknown>[]).map((item: Record<string, unknown>) => ({
338
337
  [param]: item[field] ?? item.id ?? String(item),
339
338
  }));
340
339
  }
@@ -54,6 +54,7 @@ import type {
54
54
  JxAttributeValue,
55
55
  JxElement,
56
56
  JxHeadEntry,
57
+ JxMappedArray,
57
58
  JxMutableNode,
58
59
  JxStateDefinition,
59
60
  JxStyle,
@@ -188,8 +189,7 @@ export async function buildSite(
188
189
  }
189
190
 
190
191
  // ── 5b. Collect server entries from components (for site-wide bundling) ──
191
- /** @type {{ exportName: string; src: string }[]} */
192
- const siteServerEntries = [];
192
+ const siteServerEntries: { exportName: string; src: string }[] = [];
193
193
  if (projectConfig.build.adapter) {
194
194
  for (const [, doc] of componentDefs) {
195
195
  const entries = collectServerEntries(doc);
@@ -207,8 +207,7 @@ export async function buildSite(
207
207
 
208
208
  const cfImages = projectConfig.images.optimize && projectConfig.images.service === "cloudflare";
209
209
  const imageCache = projectConfig.images.optimize && !cfImages ? loadCache(projectRoot) : null;
210
- /** @type {import("./image-transform.ts").ImageMetaCache | null} */
211
- const imageMetaCache = cfImages ? new Map() : null;
210
+ const imageMetaCache: ImageMetaCache | null = cfImages ? new Map() : null;
212
211
  if (cfImages) {
213
212
  console.log(
214
213
  `images.service is "cloudflare" — srcsets use /cdn-cgi/image transform URLs. ` +
@@ -274,7 +273,7 @@ export async function buildSite(
274
273
  mode: "export",
275
274
  })) as string;
276
275
  if (content) {
277
- const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]);
276
+ const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]!);
278
277
  writeFileSync(sidecarPath, content, "utf8");
279
278
  fileCount += 1;
280
279
  }
@@ -326,7 +325,7 @@ export async function buildSite(
326
325
  const { adapter } = projectConfig.build;
327
326
  log("Generating site-wide server worker...");
328
327
 
329
- const deduped = new Map();
328
+ const deduped = new Map<string, { exportName: string; src: string }>();
330
329
  for (const entry of siteServerEntries) {
331
330
  if (!deduped.has(entry.exportName)) {
332
331
  deduped.set(entry.exportName, entry);
@@ -460,7 +459,7 @@ async function compilePage(
460
459
  // Determine the page title — resolve template strings against the scope
461
460
  let title = pageTitle ?? projectConfig.name ?? "Jx Site";
462
461
  if (typeof title === "string" && isTemplateString(title)) {
463
- title = evaluateStaticTemplate(title, scope) ?? (title as string);
462
+ title = (evaluateStaticTemplate(title, scope) as string | null) ?? (title as string);
464
463
  }
465
464
 
466
465
  // Resolve template strings in $head entries
@@ -595,14 +594,16 @@ function resolveHeadTemplates(headEntries: JxHeadEntry[], scope: Record<string,
595
594
  resolved.attributes = { ...resolved.attributes };
596
595
  for (const [k, v] of Object.entries(resolved.attributes)) {
597
596
  if (typeof v === "string" && isTemplateString(v)) {
598
- resolved.attributes[k] = evaluateStaticTemplate(v, scope) ?? (v as string | boolean);
597
+ resolved.attributes[k] =
598
+ (evaluateStaticTemplate(v, scope) as string | boolean | null) ??
599
+ (v as string | boolean);
599
600
  }
600
601
  }
601
602
  }
602
603
  if (typeof resolved.textContent === "string" && isTemplateString(resolved.textContent)) {
603
604
  resolved.textContent =
604
- evaluateStaticTemplate(resolved.textContent, scope) ??
605
- (resolved.textContent as string | undefined);
605
+ (evaluateStaticTemplate(resolved.textContent, scope) as string | null) ??
606
+ resolved.textContent;
606
607
  }
607
608
  return resolved;
608
609
  });
@@ -662,11 +663,11 @@ function expandMapTemplate(template: JxElement, scope: Record<string, unknown>):
662
663
  const node = {} as JxElement;
663
664
  for (const [k, v] of Object.entries(template)) {
664
665
  if (k === "children" && Array.isArray(v)) {
665
- node.children = v.map((child) => {
666
+ node.children = (v as (string | JxElement)[]).map((child) => {
666
667
  if (typeof child === "string") {
667
668
  return child;
668
669
  }
669
- return expandMapTemplate(/** @type {JxMutableNode} */ child, scope);
670
+ return expandMapTemplate(child, scope);
670
671
  });
671
672
  } else if (k === "style" && v && typeof v === "object") {
672
673
  const style: JxStyle = { ...(v as JxStyle) };
@@ -718,10 +719,26 @@ function evaluateMapTemplate(str: string, scope: Record<string, unknown>) {
718
719
  const index = (scope.$map as Record<string, unknown>)?.index;
719
720
  const singleExprMatch = str.match(/^\$\{(.+)\}$/s);
720
721
  if (singleExprMatch) {
721
- const fn = new Function("state", "$map", "item", "index", `return (${singleExprMatch[1]})`);
722
+ const fn = new Function(
723
+ "state",
724
+ "$map",
725
+ "item",
726
+ "index",
727
+ `return (${singleExprMatch[1]})`,
728
+ ) as (
729
+ state: Record<string, unknown>,
730
+ $map: unknown,
731
+ item: unknown,
732
+ index: unknown,
733
+ ) => unknown;
722
734
  return fn(scope, scope.$map, item, index);
723
735
  }
724
- const fn = new Function("state", "$map", "item", "index", `return \`${str}\``);
736
+ const fn = new Function("state", "$map", "item", "index", `return \`${str}\``) as (
737
+ state: Record<string, unknown>,
738
+ $map: unknown,
739
+ item: unknown,
740
+ index: unknown,
741
+ ) => unknown;
725
742
  return fn(scope, scope.$map, item, index);
726
743
  } catch {
727
744
  return null;
@@ -751,49 +768,38 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
751
768
  }
752
769
  if (typeof node.textContent === "string" && isTemplateString(node.textContent)) {
753
770
  node.textContent =
754
- evaluateStaticTemplate(node.textContent, scope) ?? (node.textContent as string | null);
771
+ (evaluateStaticTemplate(node.textContent, scope) as string | null) ??
772
+ (node.textContent as string | null);
755
773
  }
756
774
  if (node.style && typeof node.style === "object") {
757
775
  for (const [k, v] of Object.entries(node.style)) {
758
776
  if (typeof v === "string" && isTemplateString(v)) {
759
- node.style[k] = evaluateStaticTemplate(v, scope) ?? (v as string | number | JxStyle);
777
+ node.style[k] =
778
+ (evaluateStaticTemplate(v, scope) as string | number | JxStyle | undefined) ??
779
+ (v as string | number | JxStyle);
760
780
  }
761
781
  }
762
782
  }
763
783
  if (node.attributes && typeof node.attributes === "object") {
764
784
  for (const [k, v] of Object.entries(node.attributes)) {
765
785
  if (typeof v === "string" && isTemplateString(v)) {
766
- node.attributes[k] = evaluateStaticTemplate(v, scope) ?? v;
786
+ node.attributes[k] = (evaluateStaticTemplate(v, scope) as JxAttributeValue | null) ?? v;
767
787
  }
768
788
  }
769
789
  }
770
790
  if (node.$props && typeof node.$props === "object") {
771
791
  for (const [k, v] of Object.entries(node.$props)) {
772
792
  if (typeof v === "string" && isTemplateString(v)) {
773
- node.$props[k] = evaluateStaticTemplate(v, scope) ?? v;
793
+ node.$props[k] = (evaluateStaticTemplate(v, scope) as JsonValue | null) ?? v;
774
794
  }
775
795
  }
776
796
  }
777
797
  const rawChildren = node.children;
798
+ // Legacy whole-children repeater: expand the items into the node's static children.
778
799
  if (isMappedArray(rawChildren)) {
779
- const itemsSrc = rawChildren.items;
780
- let items = null;
781
- if (isRef(itemsSrc)) {
782
- items = resolveRefValue(itemsSrc.$ref, scope);
783
- } else if (Array.isArray(itemsSrc)) {
784
- items = itemsSrc;
785
- }
786
- const mapTemplate = rawChildren.map;
787
- if (Array.isArray(items) && mapTemplate) {
788
- node.children = items.map((item, index) => {
789
- const childScope = Object.create(scope);
790
- childScope.$map = { index, item };
791
- childScope["$map/item"] = item;
792
- childScope["$map/index"] = index;
793
- const expanded = expandMapTemplate(mapTemplate, childScope);
794
- resolveDocTemplates(expanded, childScope);
795
- return expanded;
796
- });
800
+ const expanded = expandMappedArrayStatic(rawChildren, scope);
801
+ if (expanded) {
802
+ node.children = expanded;
797
803
  return;
798
804
  }
799
805
  }
@@ -809,7 +815,7 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
809
815
  } else if (Array.isArray(node.children)) {
810
816
  let i = 0;
811
817
  while (i < node.children.length) {
812
- const child = node.children[i];
818
+ const child = node.children[i]!;
813
819
  if (typeof child === "string" && isTemplateString(child)) {
814
820
  const resolved = evaluateStaticTemplate(child, scope);
815
821
  if (Array.isArray(resolved)) {
@@ -822,12 +828,55 @@ function resolveDocTemplates(node: JxElement | string, scope: Record<string, unk
822
828
  continue;
823
829
  }
824
830
  }
831
+ // Array pseudo-element among siblings: expand its items in place.
832
+ if (isMappedArray(child)) {
833
+ const expanded = expandMappedArrayStatic(child, scope);
834
+ if (expanded) {
835
+ node.children.splice(i, 1, ...expanded);
836
+ i += expanded.length;
837
+ continue;
838
+ }
839
+ }
825
840
  resolveDocTemplates(child, scope);
826
841
  i += 1;
827
842
  }
828
843
  }
829
844
  }
830
845
 
846
+ /**
847
+ * Statically expand a mapped array to its resolved item nodes when `items` resolves to an array at
848
+ * build time, or null otherwise (leave the array node for client-side rendering).
849
+ *
850
+ * @param {JxMappedArray} arrayDef
851
+ * @param {Record<string, unknown>} scope
852
+ * @returns {(JxElement | string)[] | null}
853
+ */
854
+ function expandMappedArrayStatic(
855
+ arrayDef: JxMappedArray,
856
+ scope: Record<string, unknown>,
857
+ ): (JxElement | string)[] | null {
858
+ const itemsSrc = arrayDef.items;
859
+ let items: unknown = null;
860
+ if (isRef(itemsSrc)) {
861
+ items = resolveRefValue(itemsSrc.$ref, scope);
862
+ } else if (Array.isArray(itemsSrc)) {
863
+ items = itemsSrc;
864
+ }
865
+ const mapTemplate = arrayDef.map;
866
+ if (!Array.isArray(items) || !mapTemplate) {
867
+ return null;
868
+ }
869
+ return (items as unknown[]).map((item: unknown, index) => {
870
+ const childScope = Object.create(scope) as Record<string, unknown>;
871
+ childScope.$map = { index, item };
872
+ childScope["$map/item"] = item;
873
+ childScope["$map/index"] = index;
874
+ const expanded = expandMapTemplate(mapTemplate, childScope);
875
+ resolveDocTemplates(expanded, childScope);
876
+ return expanded;
877
+ });
878
+ }
879
+
831
880
  /** Collector for CSS rules extracted from component slot content during expansion. */
832
881
  interface SlotCssCollector {
833
882
  rules: string[];
@@ -853,7 +902,7 @@ function expandComponents(
853
902
  return;
854
903
  }
855
904
  if (Array.isArray(node)) {
856
- for (const n of node) {
905
+ for (const n of node as (JxElement | string)[]) {
857
906
  expandComponents(n, componentDefs, slotCss);
858
907
  }
859
908
  return;
@@ -1012,11 +1061,11 @@ function injectHead(html: string, headEntries: JxHeadEntry[], lang: string) {
1012
1061
  const existingMatch = html.match(headPattern);
1013
1062
  let preservedBlocks = "";
1014
1063
  if (existingMatch) {
1015
- const styles = existingMatch[1].match(/<style>[\s\S]*?<\/style>/gi);
1064
+ const styles = existingMatch[1]!.match(/<style>[\s\S]*?<\/style>/gi);
1016
1065
  if (styles) {
1017
1066
  preservedBlocks += `\n ${styles.join("\n ")}`;
1018
1067
  }
1019
- const scripts = existingMatch[1].match(/<script[\s\S]*?<\/script>/gi);
1068
+ const scripts = existingMatch[1]!.match(/<script[\s\S]*?<\/script>/gi);
1020
1069
  if (scripts) {
1021
1070
  preservedBlocks += `\n ${scripts.join("\n ")}`;
1022
1071
  }