@jxsuite/compiler 0.28.0 → 0.28.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +166 -249
- package/dist/compiler.js +100 -188
- package/package.json +5 -5
- package/src/cli.ts +6 -6
- package/src/compile-cli.ts +6 -4
- package/src/compiler.ts +35 -29
- package/src/shared.ts +402 -231
- package/src/site/content-loader.ts +54 -29
- package/src/site/context-injection.ts +13 -9
- package/src/site/format-host.ts +26 -15
- package/src/site/head-merger.ts +37 -18
- package/src/site/image-cache.ts +13 -12
- package/src/site/image-optimizer.ts +32 -23
- package/src/site/image-transform.ts +172 -81
- package/src/site/layout-resolver.ts +33 -18
- package/src/site/pages-discovery.ts +61 -39
- package/src/site/prototype-resolver.ts +45 -21
- package/src/site/site-build.ts +232 -183
- package/src/site/site-loader.ts +71 -52
- package/src/targets/compile-class.ts +70 -51
- package/src/targets/compile-client.ts +271 -227
- package/src/targets/compile-element.ts +230 -146
- package/src/targets/compile-server.ts +16 -62
- package/src/targets/compile-static.ts +27 -19
- package/src/targets/compile-image-endpoint.ts +0 -138
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
* file.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
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
|
|
33
|
+
const doc = (await dereference(sourcePath)) as JxElement;
|
|
36
34
|
const entries = collectServerEntries(doc);
|
|
37
|
-
if (entries.length === 0)
|
|
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"`,
|
|
67
|
-
*
|
|
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
|
|
80
|
-
if (entries.length === 0 && !adapter)
|
|
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
|
-
|
|
89
|
-
//
|
|
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
|
-
|
|
98
|
+
|
|
101
99
|
const app = new Hono()
|
|
102
|
-
${routes}
|
|
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 {
|
|
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 {
|
|
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:
|
|
29
|
-
opts:
|
|
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
|
|
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 {
|
|
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:
|
|
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")
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
}
|