@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.
- package/dist/cli.js +141 -351
- package/dist/compiler.js +100 -304
- package/package.json +6 -8
- package/src/compiler.ts +25 -13
- package/src/shared.ts +14 -7
- package/src/site/content-loader.ts +104 -229
- package/src/site/context-injection.ts +1 -1
- package/src/site/format-host.ts +84 -0
- package/src/site/head-merger.ts +4 -1
- package/src/site/image-optimizer.ts +7 -0
- package/src/site/image-transform.ts +167 -43
- package/src/site/pages-discovery.ts +66 -31
- package/src/site/prototype-resolver.ts +17 -9
- package/src/site/site-build.ts +101 -70
- package/src/site/site-loader.ts +11 -0
- package/src/targets/compile-client.ts +32 -7
- package/src/targets/compile-element.ts +20 -10
- package/src/targets/compile-server.ts +5 -46
- package/src/targets/compile-markdown.ts +0 -977
|
@@ -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: {
|
|
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: {
|
|
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
|
|
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
|
|
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] ??
|
|
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
|
}
|
package/src/site/site-build.ts
CHANGED
|
@@ -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
|
|
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
|
|
129
|
-
|
|
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, {
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
500
|
+
if (projectConfig.images?.optimize && (imageCache || imageMetaCache)) {
|
|
476
501
|
await transformImageNodes(
|
|
477
502
|
layoutDoc,
|
|
478
|
-
projectConfig.images as
|
|
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[]} */
|
|
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 {
|
|
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} */
|
|
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>;
|
package/src/site/site-loader.ts
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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: {
|
|
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, {
|
|
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} */
|
|
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} */
|
|
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: {
|
|
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(".
|
|
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, {
|
|
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} */
|
|
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} */
|
|
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} */
|
|
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} */
|
|
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"`,
|
|
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
|
-
}
|