@jxsuite/compiler 0.27.0 → 0.28.2

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.
@@ -16,8 +16,8 @@
16
16
 
17
17
  import { readFileSync } from "node:fs";
18
18
  import { dirname, resolve } from "node:path";
19
- import { pathToFileURL } from "node:url";
20
19
  import { createRequire } from "node:module";
20
+ import { importImplementation } from "./format-host.ts";
21
21
  import type { JxDocument, JxMutableNode } from "@jxsuite/schema/types";
22
22
 
23
23
  /**
@@ -59,7 +59,10 @@ export async function resolvePrototypes(
59
59
  doc: JxMutableNode | JxDocument,
60
60
  route: { sourcePath?: string; _pathParams?: Record<string, string> },
61
61
  projectRoot: string,
62
- projectContext: { config?: Record<string, unknown>; contentTypes?: Map<string, unknown[]> } = {},
62
+ projectContext: {
63
+ config?: Record<string, unknown>;
64
+ contentTypes?: Map<string, unknown[]>;
65
+ } = {},
63
66
  ) {
64
67
  const imports = doc.imports ?? {};
65
68
  const state = doc.state;
@@ -83,7 +86,7 @@ export async function resolvePrototypes(
83
86
  const resolved = await resolveClassPrototype(def, route, projectRoot, state, projectContext);
84
87
  // Preserve timing metadata on the resolved value so compilePage() can strip it
85
88
  if (def.timing && resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
86
- resolved.timing = def.timing;
89
+ (resolved as Record<string, unknown>).timing = def.timing;
87
90
  }
88
91
  state[key] = resolved;
89
92
  } catch (err) {
@@ -110,7 +113,10 @@ async function resolveClassPrototype(
110
113
  route: { sourcePath?: string; _pathParams?: Record<string, string> },
111
114
  projectRoot: string,
112
115
  state: Record<string, unknown>,
113
- projectContext: { config?: Record<string, unknown>; contentTypes?: Map<string, unknown[]> },
116
+ projectContext: {
117
+ config?: Record<string, unknown>;
118
+ contentTypes?: Map<string, unknown[]>;
119
+ },
114
120
  ) {
115
121
  const src = def.$src;
116
122
 
@@ -136,15 +142,17 @@ async function resolveClassPrototype(
136
142
  }
137
143
 
138
144
  // 3. Resolve $implementation relative to .class.json location
139
- const classJsonURL = pathToFileURL(classJsonPath).href;
140
- const implURL = new URL(classDef.$implementation, classJsonURL).href;
145
+ const implPath = resolve(dirname(classJsonPath), classDef.$implementation);
141
146
 
142
- // 4. Import the module
143
- const mod = await import(implURL);
147
+ // 4. Import the module (tolerates src-vs-dist layouts and .js → .ts under Bun)
148
+ const mod = await importImplementation(implPath);
144
149
 
145
150
  // 5. Find the exported class
146
151
  const exportName = def.$export ?? classDef.title ?? def.$prototype;
147
- const ExportedClass = mod[exportName] ?? mod.default?.[exportName];
152
+ const ExportedClass = (mod[exportName] ??
153
+ (mod.default as Record<string, unknown> | undefined)?.[exportName]) as
154
+ | (new (config: Record<string, unknown>) => { resolve?: () => unknown })
155
+ | undefined;
148
156
  if (!ExportedClass) {
149
157
  throw new Error(`Module ${classDef.$implementation} does not export "${exportName}"`);
150
158
  }
@@ -9,7 +9,6 @@
9
9
  */
10
10
 
11
11
  import {
12
- readFileSync,
13
12
  writeFileSync,
14
13
  copyFileSync,
15
14
  mkdirSync,
@@ -20,11 +19,13 @@ import {
20
19
  } from "node:fs";
21
20
  import { resolve, dirname, join } from "node:path";
22
21
  import { loadProjectConfig } from "./site-loader.ts";
23
- import { discoverPages, expandDynamicRoutes } from "./pages-discovery.ts";
22
+ import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
23
+ import { buildProjectFormatRegistry } from "./format-host.ts";
24
+ import type { FormatRegistry } from "@jxsuite/schema/format-registry";
24
25
  import { resolveLayout } from "./layout-resolver.ts";
25
26
  import { mergeHead, renderHead } from "./head-merger.ts";
26
27
  import { injectContext } from "./context-injection.ts";
27
- import { compile, compileServer, compileSiteServer, compilePagesFunctions } from "../compiler.ts";
28
+ import { compile, compileServer, compileSiteServer } from "../compiler.ts";
28
29
  import { compileElement } from "../targets/compile-element.ts";
29
30
  import {
30
31
  buildInitialScope,
@@ -42,9 +43,10 @@ import {
42
43
  } from "../shared.ts";
43
44
  import { loadContentTypes, loadContentConfig, resolveContentTypeRefs } from "./content-loader.ts";
44
45
  import { resolvePrototypes } from "./prototype-resolver.ts";
45
- import { compileMarkdown } from "../targets/compile-markdown.ts";
46
46
  import { transformImageNodes } from "./image-transform.ts";
47
47
  import { loadCache, saveCache, getImageCacheDir } from "./image-cache.ts";
48
+ import type { ImageConfig } from "./image-optimizer.ts";
49
+ import type { ImageMetaCache } from "./image-transform.ts";
48
50
  import type {
49
51
  JxElement,
50
52
  JxMutableNode,
@@ -85,6 +87,16 @@ export async function buildSite(
85
87
  const publicDir = resolve(projectRoot, "public");
86
88
  const trailingSlash = projectConfig.build.trailingSlash ?? "always";
87
89
 
90
+ // ── 1b. Build the format registry from project imports ─────────────────
91
+ const formatRegistry = await buildProjectFormatRegistry(projectRoot, projectConfig);
92
+ if (formatRegistry.entries.length > 0) {
93
+ log(
94
+ ` Registered ${formatRegistry.entries.length} format(s): ${formatRegistry.entries
95
+ .map((e) => e.name)
96
+ .join(", ")}`,
97
+ );
98
+ }
99
+
88
100
  // ── 2. Clean output directory ───────────────────────────────────────────
89
101
  if (clean && existsSync(outDir)) {
90
102
  rmSync(outDir, { recursive: true, force: true });
@@ -97,12 +109,12 @@ export async function buildSite(
97
109
  }
98
110
 
99
111
  log("Discovering pages...");
100
- const staticRoutes = discoverPages(pagesDir);
112
+ const staticRoutes = await discoverPages(pagesDir, formatRegistry);
101
113
  log(` Found ${staticRoutes.length} page(s)`);
102
114
 
103
115
  // ── 3b. Load content types ─────────────────────────────────────────────
104
116
  log("Loading content types...");
105
- const contentTypes = await loadContentTypes(projectRoot, projectConfig);
117
+ const contentTypes = await loadContentTypes(projectRoot, projectConfig, formatRegistry);
106
118
  if (contentTypes.size > 0) {
107
119
  log(` Loaded ${contentTypes.size} content type(s): ${[...contentTypes.keys()].join(", ")}`);
108
120
  // Resolve cross-content-type $ref references
@@ -113,7 +125,7 @@ export async function buildSite(
113
125
  }
114
126
 
115
127
  // ── 4. Expand dynamic routes ────────────────────────────────────────────
116
- const routes = await expandDynamicRoutes(staticRoutes, projectRoot, contentTypes);
128
+ const routes = await expandDynamicRoutes(staticRoutes, projectRoot, contentTypes, formatRegistry);
117
129
  log(` ${routes.length} route(s) after expansion`);
118
130
 
119
131
  let fileCount = 0;
@@ -125,8 +137,9 @@ export async function buildSite(
125
137
  const componentDefs: Map<string, JxElement> = new Map(); // tagName → parsed component definition
126
138
  if (existsSync(componentsDir)) {
127
139
  log("Compiling components...");
128
- const componentFiles = readdirSync(componentsDir).filter(
129
- (f: string) => f.endsWith(".json") || f.endsWith(".md"),
140
+ const componentExtensions = [".json", ...formatRegistry.documentExtensions("component")];
141
+ const componentFiles = readdirSync(componentsDir).filter((f: string) =>
142
+ componentExtensions.some((ext) => f.endsWith(ext)),
130
143
  );
131
144
  const componentOutDir = resolve(outDir, "components");
132
145
  mkdirSync(componentOutDir, { recursive: true });
@@ -134,7 +147,10 @@ export async function buildSite(
134
147
  for (const file of componentFiles) {
135
148
  try {
136
149
  const componentPath = resolve(componentsDir, file);
137
- const result = await compileElement(componentPath, { $media: projectConfig.$media });
150
+ const result = await compileElement(componentPath, {
151
+ $media: projectConfig.$media,
152
+ formats: formatRegistry,
153
+ });
138
154
  for (const f of result.files) {
139
155
  const outName = f.path.includes("/") ? (f.path.split("/").pop() as string) : f.path;
140
156
  writeFileSync(resolve(componentOutDir, outName), f.content, "utf8");
@@ -143,11 +159,7 @@ export async function buildSite(
143
159
  }
144
160
 
145
161
  // Pre-render component HTML scaffold and CSS sidecar
146
- const doc = componentPath.endsWith(".md")
147
- ? (await import("@jxsuite/parser/transpile")).transpileJxMarkdown(
148
- readFileSync(componentPath, "utf8"),
149
- )
150
- : JSON.parse(readFileSync(componentPath, "utf8"));
162
+ const doc = await readPageDocument(componentPath, formatRegistry);
151
163
  if (doc.tagName) {
152
164
  componentDefs.set(doc.tagName, doc);
153
165
  const css = buildComponentCSS(doc.tagName, doc.style, doc, projectConfig.$media ?? {});
@@ -176,14 +188,27 @@ export async function buildSite(
176
188
  const entries = collectServerEntries(doc);
177
189
  for (const entry of entries) {
178
190
  const resolvedSrc = "./components/" + entry.src.replace(/^\.\//, "");
179
- siteServerEntries.push({ exportName: entry.exportName, src: resolvedSrc });
191
+ siteServerEntries.push({
192
+ exportName: entry.exportName,
193
+ src: resolvedSrc,
194
+ });
180
195
  }
181
196
  }
182
197
  }
183
198
 
184
199
  // ── 6. Compile each route ───────────────────────────────────────────────
185
200
 
186
- const imageCache = projectConfig.images.optimize ? loadCache(projectRoot) : null;
201
+ const cfImages = projectConfig.images.optimize && projectConfig.images.service === "cloudflare";
202
+ const imageCache = projectConfig.images.optimize && !cfImages ? loadCache(projectRoot) : null;
203
+ /** @type {import("./image-transform.ts").ImageMetaCache | null} */
204
+ const imageMetaCache = cfImages ? new Map() : null;
205
+ if (cfImages) {
206
+ console.log(
207
+ `images.service is "cloudflare" — srcsets use /cdn-cgi/image transform URLs. ` +
208
+ `Ensure Image Transformations are enabled for your zone (Cloudflare dashboard → ` +
209
+ `Images → Transformations); these URLs do not work on *.pages.dev / *.workers.dev previews.`,
210
+ );
211
+ }
187
212
 
188
213
  for (const route of routes) {
189
214
  try {
@@ -195,6 +220,8 @@ export async function buildSite(
195
220
  contentTypes,
196
221
  imageCache,
197
222
  componentDefs,
223
+ imageMetaCache,
224
+ formatRegistry,
198
225
  );
199
226
 
200
227
  // Determine which component tags are fully static (for script omission)
@@ -219,17 +246,29 @@ export async function buildSite(
219
246
  writeFileSync(outPath, result.html, "utf8");
220
247
  fileCount++;
221
248
 
222
- // Write markdown export alongside HTML
223
- try {
224
- const md = compileMarkdown(result.doc, componentDefs);
225
- if (md.content) {
226
- const mdPath = outPath.replace(/\.html$/, ".md");
227
- writeFileSync(mdPath, md.content, "utf8");
228
- fileCount++;
249
+ // Write serialized export sidecars alongside HTML (formats with exportTarget: true)
250
+ for (const fmt of formatRegistry.withCapability("serialize")) {
251
+ if (!fmt.exportTarget) continue;
252
+ try {
253
+ const content = (await fmt.call("serialize", result.doc, {
254
+ mode: "export",
255
+ componentDefs,
256
+ evaluateTemplate: (value: string, scope: Record<string, unknown>) => {
257
+ if (!isTemplateString(value)) return undefined;
258
+ return evaluateStaticTemplate(value, scope) ?? value;
259
+ },
260
+ buildScope: (state: Record<string, JxStateDefinition>) =>
261
+ buildInitialScope(state, null),
262
+ })) as string;
263
+ if (content) {
264
+ const sidecarPath = outPath.replace(/\.html$/, fmt.extensions[0]);
265
+ writeFileSync(sidecarPath, content, "utf8");
266
+ fileCount++;
267
+ }
268
+ } catch (e) {
269
+ const err = e as Error;
270
+ errors.push(`Error exporting ${fmt.name} for ${route.urlPattern}: ${err.message}`);
229
271
  }
230
- } catch (e) {
231
- const err = e as Error;
232
- errors.push(`Error exporting markdown for ${route.urlPattern}: ${err.message}`);
233
272
  }
234
273
 
235
274
  // Write any additional files (island modules, etc.)
@@ -279,17 +318,29 @@ export async function buildSite(
279
318
  if (!deduped.has(entry.exportName)) deduped.set(entry.exportName, entry);
280
319
  }
281
320
 
282
- if (adapter === "cloudflare-pages") {
283
- const functions = compilePagesFunctions([...deduped.values()]);
321
+ // Cloudflare Pages uses advanced mode (_worker.js inside the build output) — the
322
+ // functions/ directory convention only works from the project root, not from dist/.
323
+ // A static-only Pages site needs no worker at all.
324
+ const skipWorker = adapter === "cloudflare-pages" && deduped.size === 0;
325
+ const workerSource = skipWorker ? null : compileSiteServer([...deduped.values()], { adapter });
284
326
 
285
- for (const [filePath, source] of functions) {
286
- const fullPath = resolve(outDir, filePath);
287
- mkdirSync(resolve(fullPath, ".."), { recursive: true });
288
- writeFileSync(fullPath, source, "utf8");
327
+ if (workerSource) {
328
+ const workerName = adapter === "cloudflare-pages" ? "_worker.js" : "worker.js";
329
+ writeFileSync(resolve(outDir, workerName), workerSource, "utf8");
330
+ fileCount++;
331
+ log(` Generated dist/${workerName} (${deduped.size} server function(s))`);
332
+
333
+ if (adapter === "cloudflare-pages") {
334
+ // Only invoke the worker for server routes; everything else stays static.
335
+ writeFileSync(
336
+ resolve(outDir, "_routes.json"),
337
+ JSON.stringify({ version: 1, include: ["/_jx/*"], exclude: [] }, null, 2) + "\n",
338
+ "utf8",
339
+ );
289
340
  fileCount++;
290
341
  }
291
342
 
292
- // Copy server source files so function imports resolve
343
+ // Copy server source files into dist/components/ so worker imports resolve
293
344
  const distComponentsDir = resolve(outDir, "components");
294
345
  mkdirSync(distComponentsDir, { recursive: true });
295
346
  for (const { src } of deduped.values()) {
@@ -299,29 +350,6 @@ export async function buildSite(
299
350
  copyFileSync(srcFile, destFile);
300
351
  }
301
352
  }
302
-
303
- if (functions.size > 0) {
304
- log(` Generated ${functions.size} Pages function(s) in dist/functions/`);
305
- }
306
- } else {
307
- const workerSource = compileSiteServer([...deduped.values()], { adapter });
308
-
309
- if (workerSource) {
310
- const workerPath = resolve(outDir, "worker.js");
311
- writeFileSync(workerPath, workerSource, "utf8");
312
- fileCount++;
313
- log(` Generated dist/worker.js (${deduped.size} server function(s))`);
314
-
315
- // Copy server source files into dist/components/ so worker imports resolve
316
- const distComponentsDir = resolve(outDir, "components");
317
- for (const { src } of deduped.values()) {
318
- const srcFile = resolve(projectRoot, src.replace(/^\.\//, ""));
319
- const destFile = resolve(distComponentsDir, src.replace(/^\.\/components\//, ""));
320
- if (existsSync(srcFile)) {
321
- copyFileSync(srcFile, destFile);
322
- }
323
- }
324
- }
325
353
  }
326
354
  }
327
355
 
@@ -369,6 +397,7 @@ export async function buildSite(
369
397
  * @param {Map<string, ContentLoaderEntry[]>} [contentTypes]
370
398
  * @param {import("./image-cache.js").CacheManifest | null} [imageCache]
371
399
  * @param {Map<string, JxElement>} [componentDefs]
400
+ * @param {ImageMetaCache | null} [imageMetaCache] - Set when images.service is "cloudflare"
372
401
  * @returns {Promise<{
373
402
  * html: string;
374
403
  * files: { path: string; content: string; tagName?: string }[];
@@ -383,15 +412,11 @@ async function compilePage(
383
412
  contentTypes: Map<string, ContentLoaderEntry[]> = new Map(),
384
413
  imageCache: import("./image-cache.js").CacheManifest | null = null,
385
414
  componentDefs: Map<string, JxElement> = new Map(),
415
+ imageMetaCache: ImageMetaCache | null = null,
416
+ formatRegistry?: FormatRegistry,
386
417
  ) {
387
- // Load the raw page document
388
- let pageDoc;
389
- if ((route.sourcePath as string).endsWith(".md")) {
390
- const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
391
- pageDoc = transpileJxMarkdown(readFileSync(route.sourcePath as string, "utf8"));
392
- } else {
393
- pageDoc = JSON.parse(readFileSync(route.sourcePath as string, "utf8"));
394
- }
418
+ // Load the raw page document (.json natively, other formats via the registry)
419
+ const pageDoc = await readPageDocument(route.sourcePath as string, formatRegistry);
395
420
 
396
421
  // Resolve layout (wraps page in layout with slot distribution)
397
422
  const layoutDoc = resolveLayout(pageDoc, projectConfig, projectRoot);
@@ -472,12 +497,13 @@ async function compilePage(
472
497
  }
473
498
 
474
499
  // Transform <img> nodes for responsive image optimization
475
- if (imageCache && projectConfig.images?.optimize) {
500
+ if (projectConfig.images?.optimize && (imageCache || imageMetaCache)) {
476
501
  await transformImageNodes(
477
502
  layoutDoc,
478
- projectConfig.images as import("./image-optimizer").ImageConfig,
503
+ projectConfig.images as ImageConfig,
479
504
  projectRoot,
480
505
  imageCache,
506
+ imageMetaCache ?? undefined,
481
507
  );
482
508
  }
483
509
 
@@ -496,7 +522,7 @@ async function compilePage(
496
522
  (e: JxElement | string) => typeof e === "string" && !e.startsWith("./") && !e.startsWith("../"),
497
523
  );
498
524
  if (npmElements.length > 0) {
499
- result.html = injectNpmElementScripts(result.html, /** @type {string[]} */ (npmElements));
525
+ result.html = injectNpmElementScripts(result.html, /** @type {string[]} */ npmElements);
500
526
  }
501
527
 
502
528
  // Compile server handler if applicable (skip when provider bundles site-wide)
@@ -512,7 +538,12 @@ async function compilePage(
512
538
  }
513
539
  }
514
540
 
515
- return { html: result.html, files: result.files, serverHandler, doc: layoutDoc };
541
+ return {
542
+ html: result.html,
543
+ files: result.files,
544
+ serverHandler,
545
+ doc: layoutDoc,
546
+ };
516
547
  }
517
548
 
518
549
  /**
@@ -595,7 +626,7 @@ function expandMapTemplate(template: JxMutableNode, scope: Record<string, unknow
595
626
  if (k === "children" && Array.isArray(v)) {
596
627
  node.children = v.map((child) => {
597
628
  if (typeof child === "string") return child;
598
- return expandMapTemplate(/** @type {JxMutableNode} */ (child), scope);
629
+ return expandMapTemplate(/** @type {JxMutableNode} */ child, scope);
599
630
  });
600
631
  } else if (k === "style" && v && typeof v === "object") {
601
632
  const style = { ...v } as Record<string, unknown>;
@@ -34,6 +34,7 @@ const DEFAULTS = {
34
34
  quality: { webp: 80, avif: 65, jpeg: 80, png: 80 },
35
35
  sizes: "(max-width: 768px) 100vw, 50vw",
36
36
  lazyLoad: true,
37
+ service: "build",
37
38
  },
38
39
  build: {
39
40
  outDir: "./dist",
@@ -96,6 +97,16 @@ export function loadProjectConfig(projectRoot: string) {
96
97
  );
97
98
  }
98
99
 
100
+ // Validate image service. The "cloudflare" service emits /cdn-cgi/image transform URLs,
101
+ // which work on any zone served through Cloudflare regardless of the build adapter.
102
+ const VALID_IMAGE_SERVICES = ["build", "cloudflare"];
103
+ if (!VALID_IMAGE_SERVICES.includes(config.images.service)) {
104
+ throw new Error(
105
+ `Unknown images.service "${config.images.service}" in project.json. ` +
106
+ `Valid services: ${VALID_IMAGE_SERVICES.join(", ")}`,
107
+ );
108
+ }
109
+
99
110
  return {
100
111
  config,
101
112
  configPath,
@@ -88,10 +88,16 @@ export function compileClient(
88
88
  if ("$expression" in d) {
89
89
  const node = (d as Record<string, unknown>).$expression as ExpressionNode;
90
90
  if (isMutating(node.operator)) {
91
- const compiled = compileExpression(node, { statePrefix: "state", eventParam: "e" });
91
+ const compiled = compileExpression(node, {
92
+ statePrefix: "state",
93
+ eventParam: "e",
94
+ });
92
95
  onEntries.push([key, { args: ["state", "e"], body: compiled }]);
93
96
  } else {
94
- const compiled = compileExpression(node, { statePrefix: "state", eventParam: "e" });
97
+ const compiled = compileExpression(node, {
98
+ statePrefix: "state",
99
+ eventParam: "e",
100
+ });
95
101
  computedEntries.push([key, `() => ${compiled}`]);
96
102
  }
97
103
  continue;
@@ -235,7 +241,15 @@ function buildClientNode(
235
241
  context: any,
236
242
  bindings: Map<string, string>,
237
243
  handlers: Map<string, any>,
238
- counter: { t: number; s: number; h: number; m: number; sw: number; l: number; needsLit: boolean },
244
+ counter: {
245
+ t: number;
246
+ s: number;
247
+ h: number;
248
+ m: number;
249
+ sw: number;
250
+ l: number;
251
+ needsLit: boolean;
252
+ },
239
253
  ) {
240
254
  // String children are text nodes
241
255
  if (typeof def === "string") {
@@ -285,9 +299,12 @@ function buildClientNode(
285
299
  const v = val as JxMutableNode;
286
300
  const key = `_h${counter.h++}`;
287
301
  bindAttrs.push(`@${eventName}="${key}"`);
288
- handlers.set(key, { args: v.parameters ?? v.arguments ?? ["state", "event"], body: v.body });
302
+ handlers.set(key, {
303
+ args: v.parameters ?? v.arguments ?? ["state", "event"],
304
+ body: v.body,
305
+ });
289
306
  needsBind = true;
290
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
307
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
291
308
  const key = `_h${counter.h++}`;
292
309
  bindAttrs.push(`@${eventName}="${key}"`);
293
310
  const compiled = compileExpression(
@@ -513,7 +530,7 @@ function emitLitMapTemplate(def: JxMutableNode) {
513
530
  } else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
514
531
  const body = mapRefsToLit((val as JxMutableNode).body as string);
515
532
  attrs += " @" + eventName + "=${(e) => { " + body + " }}";
516
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
533
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
517
534
  const compiled = compileExpression(
518
535
  (val as Record<string, unknown>).$expression as ExpressionNode,
519
536
  {
@@ -594,7 +611,15 @@ function emitClientModule(
594
611
  onEntries: [string, any][],
595
612
  initBlocks: string[],
596
613
  srcImportMap: Map<string, Set<string>>,
597
- counter: { t: number; s: number; h: number; m: number; sw: number; l: number; needsLit: boolean },
614
+ counter: {
615
+ t: number;
616
+ s: number;
617
+ h: number;
618
+ m: number;
619
+ sw: number;
620
+ l: number;
621
+ needsLit: boolean;
622
+ },
598
623
  _reactivitySrc: string,
599
624
  ) {
600
625
  const lines: string[] = [];
@@ -25,7 +25,7 @@ import type { JxStyle, JxMutableNode } from "@jxsuite/schema/types";
25
25
  * @returns {Promise<{ files: { path: string; content: string; tagName: string }[] }>}
26
26
  */
27
27
  export async function compileElement(sourcePath: string | any, opts: Record<string, unknown> = {}) {
28
- const { resolveElementPath, $media: optsMedia } = opts as JxMutableNode;
28
+ const { resolveElementPath, $media: optsMedia, formats } = opts as JxMutableNode;
29
29
  /** @type {{ path: string; content: string; tagName: string }[]} */
30
30
  const files: { path: string; content: string; tagName: string }[] = [];
31
31
  const visited: Set<string> = new Set();
@@ -45,11 +45,18 @@ export async function compileElement(sourcePath: string | any, opts: Record<stri
45
45
  filePath = parentDir ? resolve(parentDir, srcPath) : resolve(srcPath);
46
46
  if (visited.has(filePath)) return;
47
47
  visited.add(filePath);
48
- if (filePath.endsWith(".md")) {
49
- const { transpileJxMarkdown } = await import("@jxsuite/parser/transpile");
50
- doc = transpileJxMarkdown(readFileSync(filePath, "utf8"));
51
- } else {
48
+ if (filePath.endsWith(".json")) {
52
49
  doc = JSON.parse(readFileSync(filePath, "utf8"));
50
+ } else {
51
+ const { extname } = await import("node:path");
52
+ const ext = extname(filePath).toLowerCase();
53
+ const registry = formats as import("@jxsuite/schema/format-registry").FormatRegistry;
54
+ const entry = registry?.byExtension?.(ext, "parse");
55
+ if (!entry) {
56
+ const { unknownFormatError } = await import("../site/format-host.ts");
57
+ throw unknownFormatError(filePath, ext);
58
+ }
59
+ doc = (await entry.call("parse", readFileSync(filePath, "utf8"))) as JxMutableNode;
53
60
  }
54
61
  } else {
55
62
  doc = srcPath;
@@ -272,7 +279,10 @@ export function emitElementModule(
272
279
  for (const [key, def] of functionEntries) {
273
280
  lines.push("");
274
281
  if ("$expression" in def) {
275
- const compiled = compileExpression(def.$expression, { statePrefix: "s", eventParam: "e" });
282
+ const compiled = compileExpression(def.$expression, {
283
+ statePrefix: "s",
284
+ eventParam: "e",
285
+ });
276
286
  lines.push(` this.state.${key} = (s, e) => { ${compiled}; };`);
277
287
  } else if (def.$src) {
278
288
  // $src function — wrap imported function so it receives state
@@ -429,7 +439,7 @@ function emitLitChildren(
429
439
  if (!children) return "";
430
440
 
431
441
  if ((children as JxMutableNode).$prototype === "Array") {
432
- return emitMappedArray(/** @type {JxMutableNode} */ (children), indent);
442
+ return emitMappedArray(/** @type {JxMutableNode} */ children, indent);
433
443
  }
434
444
 
435
445
  if (!Array.isArray(children)) return "";
@@ -510,7 +520,7 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
510
520
  parts.push(
511
521
  `@${eventName}="\${(e) => ${refToExpr((val as JxMutableNode).$ref as string)}(s, e)}"`,
512
522
  );
513
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
523
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
514
524
  const compiled = compileExpression(
515
525
  (val as Record<string, unknown>).$expression as ExpressionNode,
516
526
  {
@@ -521,7 +531,7 @@ function emitLitNode(def: JxMutableNode | string, indent: string) {
521
531
  parts.push(`@${eventName}="\${(e) => { ${compiled}; }}"`);
522
532
  } else if (val && typeof val === "object" && (val as JxMutableNode).$prototype === "Function") {
523
533
  parts.push(
524
- `@${eventName}="\${(e) => { ${inlineHandlerBody(/** @type {JxMutableNode} */ (val))} }}"`,
534
+ `@${eventName}="\${(e) => { ${inlineHandlerBody(/** @type {JxMutableNode} */ val)} }}"`,
525
535
  );
526
536
  }
527
537
  }
@@ -582,7 +592,7 @@ function emitMappedArray(arrayDef: JxMutableNode, indent: string) {
582
592
  parts.push(
583
593
  `@${eventName}="\${(e) => ${refToExpr((val as JxMutableNode).$ref as string)}(s, e)}"`,
584
594
  );
585
- } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ (val)) {
595
+ } else if (val && typeof val === "object" && "$expression" in /** @type {any} */ val) {
586
596
  const compiled = compileExpression(
587
597
  (val as Record<string, unknown>).$expression as ExpressionNode,
588
598
  {
@@ -61,7 +61,8 @@ export default app
61
61
  * @param {{ exportName: string; src: string }[]} entries - Resolved server entries
62
62
  * @param {object} [opts]
63
63
  * @param {string} [opts.baseUrl] - Base path for server endpoints. Default is `'/_jx/server'`
64
- * @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare"`, `"node"`, `"bun"`
64
+ * @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare-workers"`,
65
+ * `"cloudflare-pages"`, `"node"`, `"bun"`
65
66
  * @returns {string | null} Complete worker/server source string, or null
66
67
  */
67
68
  export function compileSiteServer(
@@ -80,8 +81,10 @@ export function compileSiteServer(
80
81
 
81
82
  const routes = entries.map(({ exportName }) => buildRoute(exportName, baseUrl)).join("\n");
82
83
 
84
+ // Workers static assets and Pages advanced mode (_worker.js) both route every request
85
+ // through the worker — unmatched paths must fall through to the ASSETS binding.
83
86
  const adapterBlock =
84
- adapter === "cloudflare-workers"
87
+ adapter === "cloudflare-workers" || adapter === "cloudflare-pages"
85
88
  ? "\napp.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))\n"
86
89
  : "\n";
87
90
 
@@ -108,47 +111,3 @@ app.post('${baseUrl}/${exportName}', async (c) => {
108
111
  }
109
112
  })`;
110
113
  }
111
-
112
- /**
113
- * Generate Cloudflare Pages Functions from pre-collected server entries. Returns a Map of relative
114
- * file paths (under `functions/`) to source strings.
115
- *
116
- * @param {{ exportName: string; src: string }[]} entries - Resolved server entries
117
- * @param {object} [opts]
118
- * @param {string} [opts.baseUrl] - Base path prefix. Default is `'/_jx/server'`
119
- * @returns {Map<string, string>} Map of output path → source (e.g.
120
- * `"functions/_jx/server/sendEmail.js"` → source)
121
- */
122
- export function compilePagesFunctions(
123
- entries: { exportName: string; src: string }[],
124
- opts: {
125
- baseUrl?: string;
126
- } = {},
127
- ) {
128
- const { baseUrl = "/_jx/server" } = opts;
129
- const files = new Map();
130
-
131
- for (const { exportName, src } of entries) {
132
- const relativeSrc = src.startsWith("./") ? src : `./${src}`;
133
- const funcPath = `functions${baseUrl}/${exportName}.js`;
134
- const depth = funcPath.split("/").length - 1;
135
- const importPath = "../".repeat(depth) + relativeSrc.replace(/^\.\//, "");
136
-
137
- const source = `// Generated by @jxsuite/compiler — do not edit manually
138
- import { ${exportName} } from '${importPath}'
139
-
140
- export async function onRequestPost(context) {
141
- const args = await context.request.json().catch(() => ({}))
142
- try {
143
- const result = await ${exportName}(args, context.env)
144
- return Response.json(result)
145
- } catch (e) {
146
- return Response.json({ ok: false, error: e?.message ?? 'Server error' }, { status: 500 })
147
- }
148
- }
149
- `;
150
- files.set(funcPath, source);
151
- }
152
-
153
- return files;
154
- }