@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.
@@ -5,11 +5,9 @@
5
5
  * file.
6
6
  */
7
7
 
8
- import $RefParser from "@apidevtools/json-schema-ref-parser";
8
+ import { dereference } from "@apidevtools/json-schema-ref-parser";
9
9
  import { collectServerEntries } from "../shared.ts";
10
- import { buildImageHandlerSource, buildImageWorkerRoute } from "./compile-image-endpoint.ts";
11
10
  import type { JxElement } from "@jxsuite/schema/types";
12
- import type { ImageConfig } from "../site/image-optimizer.ts";
13
11
 
14
12
  /**
15
13
  * Compile a Jx document to a Hono server handler file. The handler exposes each `timing: "server"`
@@ -32,9 +30,11 @@ export async function compileServer(
32
30
  } = {},
33
31
  ) {
34
32
  const { baseUrl = "/_jx/server" } = opts;
35
- const doc = (await $RefParser.dereference(sourcePath)) as JxElement;
33
+ const doc = (await dereference(sourcePath)) as JxElement;
36
34
  const entries = collectServerEntries(doc);
37
- if (entries.length === 0) return null;
35
+ if (entries.length === 0) {
36
+ return null;
37
+ }
38
38
 
39
39
  const imports = entries
40
40
  .map(({ exportName, src }) => `import { ${exportName} } from '${src}'`)
@@ -63,9 +63,8 @@ export default app
63
63
  * @param {{ exportName: string; src: string }[]} entries - Resolved server entries
64
64
  * @param {object} [opts]
65
65
  * @param {string} [opts.baseUrl] - Base path for server endpoints. Default is `'/_jx/server'`
66
- * @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare"`, `"node"`, `"bun"`
67
- * @param {ImageConfig | null} [opts.images] - When set, embed the Cloudflare Images binding
68
- * endpoint at `/_jx/image` (images.service: "cloudflare")
66
+ * @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare-workers"`,
67
+ * `"cloudflare-pages"`, `"node"`, `"bun"`
69
68
  * @returns {string | null} Complete worker/server source string, or null
70
69
  */
71
70
  export function compileSiteServer(
@@ -73,11 +72,12 @@ export function compileSiteServer(
73
72
  opts: {
74
73
  baseUrl?: string;
75
74
  adapter?: string | null;
76
- images?: ImageConfig | null;
77
75
  } = {},
78
76
  ) {
79
- const { baseUrl = "/_jx/server", adapter = null, images = null } = opts;
80
- if (entries.length === 0 && !adapter) return null;
77
+ const { baseUrl = "/_jx/server", adapter = null } = opts;
78
+ if (entries.length === 0 && !adapter) {
79
+ return null;
80
+ }
81
81
 
82
82
  const imports = entries
83
83
  .map(({ exportName, src }) => `import { ${exportName} } from '${src}'`)
@@ -85,21 +85,19 @@ export function compileSiteServer(
85
85
 
86
86
  const routes = entries.map(({ exportName }) => buildRoute(exportName, baseUrl)).join("\n");
87
87
 
88
- const imageHandler = images ? `\n${buildImageHandlerSource(images)}\n` : "";
89
- // The image route must register before the cloudflare-workers ASSETS catch-all below.
90
- const imageRoute = images ? buildImageWorkerRoute() : "";
91
-
88
+ // Workers static assets and Pages advanced mode (_worker.js) both route every request
89
+ // Through the worker — unmatched paths must fall through to the ASSETS binding.
92
90
  const adapterBlock =
93
- adapter === "cloudflare-workers"
91
+ adapter === "cloudflare-workers" || adapter === "cloudflare-pages"
94
92
  ? "\napp.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))\n"
95
93
  : "\n";
96
94
 
97
95
  return `// Generated by @jxsuite/compiler — do not edit manually
98
96
  import { Hono } from 'hono'
99
97
  ${imports}
100
- ${imageHandler}
98
+
101
99
  const app = new Hono()
102
- ${routes}${imageRoute}
100
+ ${routes}
103
101
  ${adapterBlock}
104
102
  export default app
105
103
  `;
@@ -117,47 +115,3 @@ app.post('${baseUrl}/${exportName}', async (c) => {
117
115
  }
118
116
  })`;
119
117
  }
120
-
121
- /**
122
- * Generate Cloudflare Pages Functions from pre-collected server entries. Returns a Map of relative
123
- * file paths (under `functions/`) to source strings.
124
- *
125
- * @param {{ exportName: string; src: string }[]} entries - Resolved server entries
126
- * @param {object} [opts]
127
- * @param {string} [opts.baseUrl] - Base path prefix. Default is `'/_jx/server'`
128
- * @returns {Map<string, string>} Map of output path → source (e.g.
129
- * `"functions/_jx/server/sendEmail.js"` → source)
130
- */
131
- export function compilePagesFunctions(
132
- entries: { exportName: string; src: string }[],
133
- opts: {
134
- baseUrl?: string;
135
- } = {},
136
- ) {
137
- const { baseUrl = "/_jx/server" } = opts;
138
- const files = new Map();
139
-
140
- for (const { exportName, src } of entries) {
141
- const relativeSrc = src.startsWith("./") ? src : `./${src}`;
142
- const funcPath = `functions${baseUrl}/${exportName}.js`;
143
- const depth = funcPath.split("/").length - 1;
144
- const importPath = "../".repeat(depth) + relativeSrc.replace(/^\.\//, "");
145
-
146
- const source = `// Generated by @jxsuite/compiler — do not edit manually
147
- import { ${exportName} } from '${importPath}'
148
-
149
- export async function onRequestPost(context) {
150
- const args = await context.request.json().catch(() => ({}))
151
- try {
152
- const result = await ${exportName}(args, context.env)
153
- return Response.json(result)
154
- } catch (e) {
155
- return Response.json({ ok: false, error: e?.message ?? 'Server error' }, { status: 500 })
156
- }
157
- }
158
- `;
159
- files.set(funcPath, source);
160
- }
161
-
162
- return files;
163
- }
@@ -6,29 +6,34 @@
6
6
  */
7
7
 
8
8
  import {
9
- isNodeDynamic,
10
- createCompileContext,
11
- resolveStaticValue,
12
9
  buildAttrs,
13
10
  compileStyles,
11
+ createCompileContext,
14
12
  escapeHtml,
13
+ isNodeDynamic,
14
+ resolveStaticValue,
15
15
  } from "../shared.ts";
16
16
  import { emitElementModule } from "./compile-element.ts";
17
- import type { JxStyle, JxMutableNode } from "@jxsuite/schema/types";
17
+ import type { JxDocument, JxMutableNode, JxStyle } from "@jxsuite/schema/types";
18
18
 
19
19
  /**
20
20
  * Compile a static document to HTML, with dynamic subtrees as islands.
21
21
  *
22
- * @param {JxMutableNode | Record<string, any>} raw - Raw JSON document (with $ref pointers
23
- * preserved)
22
+ * @param {JxDocument} raw - Raw JSON document (with $ref pointers preserved)
24
23
  * @param {Record<string, unknown>} opts
25
24
  * @returns {{ html: string; files: { path: string; content: string; tagName: string }[] }}
26
25
  */
27
26
  export function compileStaticPage(
28
- raw: JxMutableNode | Record<string, any>,
29
- opts: Record<string, unknown>,
27
+ raw: JxDocument,
28
+ opts: {
29
+ title?: string;
30
+ reactivitySrc?: string;
31
+ litHtmlSrc?: string;
32
+ projectStyle?: JxStyle | null;
33
+ [key: string]: unknown;
34
+ },
30
35
  ) {
31
- const { title, reactivitySrc, litHtmlSrc } = opts as JxMutableNode;
36
+ const { title = "Jx App", reactivitySrc, litHtmlSrc } = opts;
32
37
 
33
38
  const rootContext = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
34
39
  const styleBlock = compileStyles(
@@ -45,10 +50,10 @@ export function compileStaticPage(
45
50
  let moduleScripts = "";
46
51
  if (islands.length > 0) {
47
52
  for (const island of islands) {
48
- const moduleContent = emitElementModule(island.def, island.className, []);
53
+ const moduleContent = emitElementModule(island.def as JxDocument, island.className, []);
49
54
  files.push({
50
- path: `_islands/${island.tagName}.js`,
51
55
  content: moduleContent,
56
+ path: `_islands/${island.tagName}.js`,
52
57
  tagName: island.tagName,
53
58
  });
54
59
  }
@@ -82,7 +87,7 @@ export function compileStaticPage(
82
87
  </body>
83
88
  </html>`;
84
89
 
85
- return { html, files };
90
+ return { files, html };
86
91
  }
87
92
 
88
93
  // ─── Node compilation ─────────────────────────────────────────────────────────
@@ -95,7 +100,7 @@ export function compileStaticPage(
95
100
  * @param {boolean} dynamic
96
101
  * @param {JxMutableNode} raw
97
102
  * @param {any} context
98
- * @param {{ def: any; tagName: string; className: string }[]} islands
103
+ * @param {{ def: JxMutableNode; tagName: string; className: string }[]} islands
99
104
  * @returns {string}
100
105
  */
101
106
  function compileNode(
@@ -116,7 +121,9 @@ function compileNode(
116
121
  if (typeof def === "number" || typeof def === "boolean") {
117
122
  return escapeHtml(String(def));
118
123
  }
119
- if (!def || typeof def !== "object") return "";
124
+ if (!def || typeof def !== "object") {
125
+ return "";
126
+ }
120
127
 
121
128
  const nextContext = createCompileContext(
122
129
  raw,
@@ -130,7 +137,7 @@ function compileNode(
130
137
  const tagName = `jx-island-${n}`;
131
138
  const className = `JxIsland${n}`;
132
139
  const elementDef = { ...(raw ?? def), tagName };
133
- islands.push({ def: elementDef, tagName, className });
140
+ islands.push({ className, def: elementDef, tagName });
134
141
  return `<${tagName}></${tagName}>`;
135
142
  }
136
143
 
@@ -148,7 +155,7 @@ function compileNode(
148
155
  * @param {JxMutableNode} def
149
156
  * @param {JxMutableNode} raw
150
157
  * @param {any} context
151
- * @param {{ def: any; tagName: string; className: string }[]} islands
158
+ * @param {{ def: JxMutableNode; tagName: string; className: string }[]} islands
152
159
  * @returns {string}
153
160
  */
154
161
  function buildInnerWithIslands(
@@ -167,13 +174,14 @@ function buildInnerWithIslands(
167
174
  const value = resolveStaticValue(source.textContent, context.scope);
168
175
  return value == null ? "" : escapeHtml(String(value));
169
176
  }
170
- if (source.innerHTML)
177
+ if (source.innerHTML) {
171
178
  return (resolveStaticValue(source.innerHTML, context.scope) as string) ?? source.innerHTML;
179
+ }
172
180
  if (Array.isArray(source.children)) {
173
- const rawChildren = raw?.children;
181
+ const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
174
182
  return source.children
175
183
  .map((c, i: number) => {
176
- const child = c as unknown as JxMutableNode;
184
+ const child = c as JxMutableNode;
177
185
  const childDynamic = isNodeDynamic(child);
178
186
  const childRaw = (rawChildren?.[i] ?? c) as JxMutableNode;
179
187
  return compileNode(child, childDynamic, childRaw, context, islands);
@@ -1,138 +0,0 @@
1
- /**
2
- * Compile-image-endpoint.js — Cloudflare Images binding endpoint codegen
3
- *
4
- * Generates the `/_jx/image` runtime image-transformation handler used when `images.service:
5
- * "cloudflare"` is set in project.json. The handler fetches the original static asset via the
6
- * ASSETS binding and pipes it through the Cloudflare Images binding, negotiating the output format
7
- * from the request's Accept header.
8
- */
9
-
10
- import type { ImageConfig } from "../site/image-optimizer.ts";
11
-
12
- const MIME: Record<string, string> = {
13
- avif: "image/avif",
14
- webp: "image/webp",
15
- jpeg: "image/jpeg",
16
- png: "image/png",
17
- };
18
-
19
- /**
20
- * Generate the shared `jxHandleImage(request, env, waitUntil)` handler source. Widths, format
21
- * preference order (avif first, mirroring the build-time pipeline), per-format quality, and the
22
- * Images binding name are baked in at codegen time.
23
- *
24
- * @param {ImageConfig} images - The project's images config
25
- * @returns {string} JavaScript source defining `jxHandleImage`
26
- */
27
- export function buildImageHandlerSource(images: ImageConfig) {
28
- const binding = images.binding ?? "IMAGES";
29
- const formats = [...(images.formats ?? [])]
30
- .filter((f) => f in MIME)
31
- .sort((a, b) => Number(b === "avif") - Number(a === "avif"));
32
- const tuples = formats.map((f) => [
33
- MIME[f],
34
- images.quality?.[f as keyof ImageConfig["quality"]] ?? 80,
35
- ]);
36
-
37
- return `const JX_IMG_WIDTHS = new Set(${JSON.stringify(images.widths ?? [])})
38
- const JX_IMG_FORMATS = ${JSON.stringify(tuples)} // [mime, quality] in preference order
39
- const JX_IMG_BINDING = ${JSON.stringify(binding)}
40
- const JX_IMG_REMOTE_DOMAINS = new Set(${JSON.stringify(images.remoteDomains ?? [])})
41
-
42
- async function jxHandleImage(request, env, waitUntil) {
43
- const url = new URL(request.url)
44
- const src = url.searchParams.get('src') ?? ''
45
- const w = Number(url.searchParams.get('w') ?? '')
46
-
47
- // Same-origin asset paths, or https sources from allowlisted remote domains —
48
- // never act as an open proxy.
49
- let remoteSrc = null
50
- if (src.startsWith('https://')) {
51
- try {
52
- const remote = new URL(src)
53
- if (JX_IMG_REMOTE_DOMAINS.has(remote.hostname)) remoteSrc = remote
54
- } catch {}
55
- if (!remoteSrc) return new Response('Invalid src', { status: 400 })
56
- } else if (!src.startsWith('/') || src.startsWith('//') || src.includes('..') ||
57
- src.includes(':') || src.includes('\\\\') || src.includes('?') || src.includes('#')) {
58
- return new Response('Invalid src', { status: 400 })
59
- }
60
- if (!JX_IMG_WIDTHS.has(w)) return new Response('Invalid width', { status: 400 })
61
-
62
- const accept = request.headers.get('accept') ?? ''
63
- let target = null
64
- for (const [mime, quality] of JX_IMG_FORMATS) {
65
- if (accept.includes(mime)) { target = { mime, quality }; break }
66
- }
67
-
68
- const v = url.searchParams.get('v') ?? ''
69
- const cacheKey = new Request(url.origin + '/_jx/image?src=' + encodeURIComponent(src) +
70
- '&w=' + w + '&f=' + (target ? target.mime : 'orig') + '&v=' + encodeURIComponent(v))
71
- const cache = caches.default
72
- const hit = await cache.match(cacheKey)
73
- if (hit) {
74
- const out = new Response(hit.body, hit)
75
- out.headers.set('Vary', 'Accept')
76
- return out
77
- }
78
-
79
- const fetchSource = () =>
80
- remoteSrc
81
- ? fetch(remoteSrc.href, { redirect: 'follow' })
82
- : env.ASSETS.fetch(new Request(new URL(src, url.origin)))
83
-
84
- const asset = await fetchSource()
85
- if (!asset.ok || !asset.body) return new Response('Not found', { status: 404 })
86
-
87
- let response
88
- if (target && env[JX_IMG_BINDING]) {
89
- try {
90
- const result = await env[JX_IMG_BINDING]
91
- .input(asset.body)
92
- .transform({ width: w, fit: 'scale-down' })
93
- .output({ format: target.mime, quality: target.quality })
94
- response = result.response()
95
- } catch (e) {
96
- // Transform failed — serve the original source untouched.
97
- const retry = await fetchSource()
98
- response = new Response(retry.body, retry)
99
- }
100
- } else {
101
- response = new Response(asset.body, asset)
102
- }
103
-
104
- response = new Response(response.body, response)
105
- response.headers.set('Cache-Control', 'public, max-age=31536000, immutable')
106
- if (waitUntil) waitUntil(cache.put(cacheKey, response.clone()))
107
- response.headers.set('Vary', 'Accept')
108
- return response
109
- }`;
110
- }
111
-
112
- /**
113
- * Generate the Hono route registering `jxHandleImage` at `/_jx/image`. Must be injected before the
114
- * cloudflare-workers `app.all('*')` ASSETS catch-all.
115
- *
116
- * @returns {string}
117
- */
118
- export function buildImageWorkerRoute() {
119
- return `
120
- app.get('/_jx/image', (c) => jxHandleImage(c.req.raw, c.env, (p) => c.executionCtx?.waitUntil?.(p)))`;
121
- }
122
-
123
- /**
124
- * Generate a complete Cloudflare Pages Function (`functions/_jx/image.js`) wrapping `jxHandleImage`
125
- * in an `onRequestGet` handler.
126
- *
127
- * @param {ImageConfig} images - The project's images config
128
- * @returns {string}
129
- */
130
- export function compilePagesImageFunction(images: ImageConfig) {
131
- return `// Generated by @jxsuite/compiler — do not edit manually
132
- ${buildImageHandlerSource(images)}
133
-
134
- export async function onRequestGet(context) {
135
- return jxHandleImage(context.request, context.env, (p) => context.waitUntil(p))
136
- }
137
- `;
138
- }