@jxsuite/compiler 0.35.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +182 -161
- package/dist/compiler.js +116 -112
- package/package.json +11 -7
- package/src/cli.ts +102 -9
- package/src/site/context-injection.ts +0 -6
- package/src/site/db-push.ts +178 -0
- package/src/site/format-host.ts +128 -13
- package/src/site/pages-discovery.ts +61 -49
- package/src/site/project-sections.ts +48 -0
- package/src/site/prototype-resolver.ts +55 -11
- package/src/site/schema-command.ts +242 -0
- package/src/site/site-build.ts +162 -26
- package/src/site/site-loader.ts +3 -5
- package/src/targets/compile-server.ts +83 -5
- package/src/site/content-loader.ts +0 -385
package/src/site/site-build.ts
CHANGED
|
@@ -23,13 +23,16 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
23
23
|
import { isMappedArray, isRef } from "@jxsuite/schema/guards";
|
|
24
24
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
25
25
|
import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
26
|
+
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
27
|
+
import { loadProjectSections } from "./project-sections.ts";
|
|
28
|
+
import type { FormatEntry, FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
29
|
+
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
28
30
|
import { resolveLayout } from "./layout-resolver.ts";
|
|
29
31
|
import { mergeHead, renderHead } from "./head-merger.ts";
|
|
30
32
|
import { injectContext } from "./context-injection.ts";
|
|
31
33
|
import { compile, compileServer, compileSiteServer } from "../compiler.ts";
|
|
32
34
|
import { compileElement } from "../targets/compile-element.ts";
|
|
35
|
+
import type { SiteConnectorSpec, SiteMountSpec } from "../targets/compile-server.ts";
|
|
33
36
|
import {
|
|
34
37
|
DEFAULT_LIT_HTML_SRC,
|
|
35
38
|
DEFAULT_REACTIVITY_SRC,
|
|
@@ -45,7 +48,6 @@ import {
|
|
|
45
48
|
resolveRefValue,
|
|
46
49
|
resolveStaticValue,
|
|
47
50
|
} from "../shared.ts";
|
|
48
|
-
import { loadContentConfig, loadContentTypes, resolveContentTypeRefs } from "./content-loader.ts";
|
|
49
51
|
import { resolvePrototypes } from "./prototype-resolver.ts";
|
|
50
52
|
import { transformImageNodes } from "./image-transform.ts";
|
|
51
53
|
import { getImageCacheDir, loadCache, saveCache } from "./image-cache.ts";
|
|
@@ -62,7 +64,6 @@ import type {
|
|
|
62
64
|
JxStyle,
|
|
63
65
|
ProjectConfig,
|
|
64
66
|
} from "@jxsuite/schema/types";
|
|
65
|
-
import type { ContentLoaderEntry } from "@jxsuite/parser/types";
|
|
66
67
|
import type { SiteRoute } from "../types.ts";
|
|
67
68
|
import type { CacheManifest } from "./image-cache.js";
|
|
68
69
|
|
|
@@ -90,13 +91,24 @@ export async function buildSite(
|
|
|
90
91
|
log("Loading project.json...");
|
|
91
92
|
const { config: projectConfig } = loadProjectConfig(projectRoot);
|
|
92
93
|
|
|
94
|
+
// The legacy imports-based content model is gone (specs/extensions.md v2) — fail loudly with
|
|
95
|
+
// The migration path instead of silently ignoring the section.
|
|
96
|
+
if ("contentTypes" in projectConfig) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`project.json declares "contentTypes", which is no longer supported. ` +
|
|
99
|
+
`Run \`bun scripts/migrate-project-extensions.ts <project>\` to migrate to ` +
|
|
100
|
+
`"extensions" + "content", then \`jx schema\`.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
const outDir = resolve(projectRoot, projectConfig.build.outDir);
|
|
94
105
|
const pagesDir = resolve(projectRoot, "pages");
|
|
95
106
|
const publicDir = resolve(projectRoot, "public");
|
|
96
107
|
const trailingSlash = projectConfig.build.trailingSlash ?? "always";
|
|
97
108
|
|
|
98
|
-
// ── 1b. Build the
|
|
99
|
-
const
|
|
109
|
+
// ── 1b. Build the extension registry from project.json#/extensions ─────
|
|
110
|
+
const registry = await buildProjectExtensionRegistry(projectRoot, projectConfig);
|
|
111
|
+
const formatRegistry = registry.formats;
|
|
100
112
|
if (formatRegistry.entries.length > 0) {
|
|
101
113
|
log(
|
|
102
114
|
` Registered ${formatRegistry.entries.length} format(s): ${formatRegistry.entries
|
|
@@ -105,6 +117,28 @@ export async function buildSite(
|
|
|
105
117
|
);
|
|
106
118
|
}
|
|
107
119
|
|
|
120
|
+
// ── 1c. Active extension server mounts (specs/extensions.md §11) ────────
|
|
121
|
+
// A mount is active when its class owns no project section, or the project declares a
|
|
122
|
+
// Non-empty value for its section key. Sites with active mounts always get a worker; a
|
|
123
|
+
// Static site cannot serve dynamic sections, so that combination is a build error.
|
|
124
|
+
const activeMounts = registry.serverMounts().filter((entry) => {
|
|
125
|
+
if (!entry.project) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
const value = projectConfig[entry.project.key] as Record<string, unknown> | undefined;
|
|
129
|
+
return value !== undefined && typeof value === "object" && Object.keys(value).length > 0;
|
|
130
|
+
});
|
|
131
|
+
if (activeMounts.length > 0 && !projectConfig.build.adapter) {
|
|
132
|
+
const sections = activeMounts
|
|
133
|
+
.map((entry) => (entry.project ? `"${entry.project.key}"` : `"${entry.server!.basePath}"`))
|
|
134
|
+
.join(", ");
|
|
135
|
+
throw new Error(
|
|
136
|
+
`project.json declares dynamic section(s) ${sections} served by extension mounts, but ` +
|
|
137
|
+
`build.adapter is "static". Dynamic tables need a server-capable adapter — set ` +
|
|
138
|
+
`build.adapter to "cloudflare-workers", "cloudflare-pages", "node", or "bun".`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
// ── 2. Clean output directory ───────────────────────────────────────────
|
|
109
143
|
if (clean && existsSync(outDir)) {
|
|
110
144
|
rmSync(outDir, { force: true, recursive: true });
|
|
@@ -120,20 +154,22 @@ export async function buildSite(
|
|
|
120
154
|
const staticRoutes = await discoverPages(pagesDir, formatRegistry);
|
|
121
155
|
log(` Found ${staticRoutes.length} page(s)`);
|
|
122
156
|
|
|
123
|
-
// ── 3b. Load
|
|
124
|
-
log("Loading
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const contentConfig = loadContentConfig(projectRoot, projectConfig);
|
|
130
|
-
if (contentConfig) {
|
|
131
|
-
resolveContentTypeRefs(contentTypes, contentConfig.config);
|
|
132
|
-
}
|
|
157
|
+
// ── 3b. Load extension-contributed project sections ────────────────────
|
|
158
|
+
log("Loading project sections...");
|
|
159
|
+
const sections = await loadProjectSections(projectRoot, projectConfig, registry);
|
|
160
|
+
const sectionKeys = Object.keys(sections);
|
|
161
|
+
if (sectionKeys.length > 0) {
|
|
162
|
+
log(` Loaded ${sectionKeys.length} section(s): ${sectionKeys.join(", ")}`);
|
|
133
163
|
}
|
|
134
164
|
|
|
135
165
|
// ── 4. Expand dynamic routes ────────────────────────────────────────────
|
|
136
|
-
const routes = await expandDynamicRoutes(
|
|
166
|
+
const routes = await expandDynamicRoutes(
|
|
167
|
+
staticRoutes,
|
|
168
|
+
projectRoot,
|
|
169
|
+
sections,
|
|
170
|
+
registry,
|
|
171
|
+
projectConfig,
|
|
172
|
+
);
|
|
137
173
|
log(` ${routes.length} route(s) after expansion`);
|
|
138
174
|
|
|
139
175
|
let fileCount = 0;
|
|
@@ -235,11 +271,12 @@ export async function buildSite(
|
|
|
235
271
|
route as unknown as SiteRoute,
|
|
236
272
|
projectConfig,
|
|
237
273
|
projectRoot,
|
|
238
|
-
|
|
274
|
+
sections,
|
|
239
275
|
imageCache,
|
|
240
276
|
componentDefs,
|
|
241
277
|
imageMetaCache,
|
|
242
278
|
formatRegistry,
|
|
279
|
+
registry,
|
|
243
280
|
);
|
|
244
281
|
|
|
245
282
|
// Determine which component tags are fully static (for script omission)
|
|
@@ -355,11 +392,22 @@ export async function buildSite(
|
|
|
355
392
|
}
|
|
356
393
|
}
|
|
357
394
|
|
|
395
|
+
// Extension server mounts: static imports of the mount modules and used connector classes,
|
|
396
|
+
// Inlined JSON options (the project's section manifest — identifiers only, never secrets).
|
|
397
|
+
const { mounts, connectors } = buildMountSpecs(activeMounts, registry, projectConfig);
|
|
398
|
+
if (mounts.length > 0) {
|
|
399
|
+
log(
|
|
400
|
+
` Mounting ${mounts.length} extension route(s): ${mounts.map((m) => m.basePath).join(", ")}`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
358
404
|
// Cloudflare Pages uses advanced mode (_worker.js inside the build output) — the
|
|
359
405
|
// Functions/ directory convention only works from the project root, not from dist/.
|
|
360
|
-
// A static-only Pages site needs no worker at all.
|
|
361
|
-
const skipWorker = adapter === "cloudflare-pages" && deduped.size === 0;
|
|
362
|
-
const workerSource = skipWorker
|
|
406
|
+
// A static-only Pages site needs no worker at all; sites with mounts always get one.
|
|
407
|
+
const skipWorker = adapter === "cloudflare-pages" && deduped.size === 0 && mounts.length === 0;
|
|
408
|
+
const workerSource = skipWorker
|
|
409
|
+
? null
|
|
410
|
+
: compileSiteServer([...deduped.values()], { adapter, connectors, mounts });
|
|
363
411
|
|
|
364
412
|
if (workerSource) {
|
|
365
413
|
const workerName = adapter === "cloudflare-pages" ? "_worker.js" : "worker.js";
|
|
@@ -445,7 +493,7 @@ export async function buildSite(
|
|
|
445
493
|
* @param {SiteRoute} route
|
|
446
494
|
* @param {ProjectConfig} projectConfig
|
|
447
495
|
* @param {string} projectRoot
|
|
448
|
-
* @param {
|
|
496
|
+
* @param {Record<string, unknown>} [sections] - Extension-loaded project sections
|
|
449
497
|
* @param {import("./image-cache.js").CacheManifest | null} [imageCache]
|
|
450
498
|
* @param {Map<string, JxElement>} [componentDefs]
|
|
451
499
|
* @param {ImageMetaCache | null} [imageMetaCache] - Set when images.service is "cloudflare"
|
|
@@ -460,11 +508,12 @@ async function compilePage(
|
|
|
460
508
|
route: SiteRoute,
|
|
461
509
|
projectConfig: ProjectConfig,
|
|
462
510
|
projectRoot: string,
|
|
463
|
-
|
|
511
|
+
sections: Record<string, unknown> = {},
|
|
464
512
|
imageCache: CacheManifest | null = null,
|
|
465
513
|
componentDefs = new Map<string, JxElement>(),
|
|
466
514
|
imageMetaCache: ImageMetaCache | null = null,
|
|
467
515
|
formatRegistry?: FormatRegistry,
|
|
516
|
+
registry?: ExtensionRegistry,
|
|
468
517
|
) {
|
|
469
518
|
// Load the raw page document (.json natively, other formats via the registry)
|
|
470
519
|
const pageDoc = await readPageDocument(route.sourcePath as string, formatRegistry);
|
|
@@ -482,12 +531,13 @@ async function compilePage(
|
|
|
482
531
|
delete layoutDoc._pageTitle;
|
|
483
532
|
|
|
484
533
|
// Inject $site and $page context
|
|
485
|
-
injectContext(layoutDoc, projectConfig, route,
|
|
534
|
+
injectContext(layoutDoc, projectConfig, route, projectRoot);
|
|
486
535
|
|
|
487
|
-
// Resolve generic $prototype entries via .class.json imports
|
|
536
|
+
// Resolve generic $prototype entries via .class.json imports (and lower registry classes)
|
|
488
537
|
await resolvePrototypes(layoutDoc, route, projectRoot, {
|
|
489
538
|
config: projectConfig,
|
|
490
|
-
|
|
539
|
+
sections,
|
|
540
|
+
...(registry === undefined ? {} : { registry }),
|
|
491
541
|
});
|
|
492
542
|
|
|
493
543
|
// Build scope from resolved state so template strings in title/$head can be evaluated
|
|
@@ -617,6 +667,92 @@ async function compilePage(
|
|
|
617
667
|
};
|
|
618
668
|
}
|
|
619
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Build the mount + connector specs for the generated worker (specs/extensions.md §11–§12).
|
|
672
|
+
*
|
|
673
|
+
* Mount options inline the extension-contributed sections from project.json — identifiers only,
|
|
674
|
+
* never secrets (§13). Connector classes are included when any section entry names their
|
|
675
|
+
* `connector.provider`; the import specifier comes from the descriptor's `connector.module` (bare
|
|
676
|
+
* specifier — Workers cannot import filesystem paths).
|
|
677
|
+
*
|
|
678
|
+
* @param {FormatEntry[]} activeMounts - Server-mount classes with active sections
|
|
679
|
+
* @param {ExtensionRegistry} registry
|
|
680
|
+
* @param {ProjectConfig} projectConfig
|
|
681
|
+
* @returns {{ mounts: SiteMountSpec[]; connectors: SiteConnectorSpec[] }}
|
|
682
|
+
*/
|
|
683
|
+
function buildMountSpecs(
|
|
684
|
+
activeMounts: FormatEntry[],
|
|
685
|
+
registry: ExtensionRegistry,
|
|
686
|
+
projectConfig: ProjectConfig,
|
|
687
|
+
): { mounts: SiteMountSpec[]; connectors: SiteConnectorSpec[] } {
|
|
688
|
+
if (activeMounts.length === 0) {
|
|
689
|
+
return { connectors: [], mounts: [] };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const sections: Record<string, unknown> = {};
|
|
693
|
+
for (const contribution of registry.projectContributions()) {
|
|
694
|
+
const { key } = contribution.project!;
|
|
695
|
+
if (key in projectConfig) {
|
|
696
|
+
sections[key] = projectConfig[key];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const mounts: SiteMountSpec[] = activeMounts.map((entry) => {
|
|
701
|
+
const server = entry.server!;
|
|
702
|
+
const { module } = server;
|
|
703
|
+
if (typeof module !== "string" || module === "") {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Extension class "${entry.name}": server.module (a bare import specifier) is required ` +
|
|
706
|
+
`to generate a deployable worker`,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
basePath: server.basePath,
|
|
711
|
+
className: (entry.classDef.title as string | undefined) ?? entry.name,
|
|
712
|
+
module,
|
|
713
|
+
options: { basePath: server.basePath, sections },
|
|
714
|
+
order: server.order ?? 100,
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Used providers: any section entry object carrying a string `provider` field (the connector
|
|
719
|
+
// Vocabulary, §12) marks that provider's class for import.
|
|
720
|
+
const usedProviders = new Set<string>();
|
|
721
|
+
for (const value of Object.values(sections)) {
|
|
722
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
for (const sectionEntry of Object.values(value as Record<string, unknown>)) {
|
|
726
|
+
const provider = (sectionEntry as { provider?: unknown } | null)?.provider;
|
|
727
|
+
if (typeof provider === "string") {
|
|
728
|
+
usedProviders.add(provider);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const connectors: SiteConnectorSpec[] = [];
|
|
734
|
+
for (const entry of registry.connectors()) {
|
|
735
|
+
const provider = String(entry.connector!.provider);
|
|
736
|
+
if (!usedProviders.has(provider)) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const { module } = entry.connector!;
|
|
740
|
+
if (typeof module !== "string" || module === "") {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`Connector class "${entry.name}": connector.module (a bare import specifier) is ` +
|
|
743
|
+
`required to generate a deployable worker`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
connectors.push({
|
|
747
|
+
className: (entry.classDef.title as string | undefined) ?? entry.name,
|
|
748
|
+
module,
|
|
749
|
+
provider,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return { connectors, mounts };
|
|
754
|
+
}
|
|
755
|
+
|
|
620
756
|
/**
|
|
621
757
|
* Resolve template strings in $head entries against the compiled scope.
|
|
622
758
|
*
|
package/src/site/site-loader.ts
CHANGED
|
@@ -39,7 +39,6 @@ const DEFAULTS = {
|
|
|
39
39
|
provider: null,
|
|
40
40
|
trailingSlash: "always",
|
|
41
41
|
},
|
|
42
|
-
contentTypes: {},
|
|
43
42
|
defaults: {
|
|
44
43
|
charset: "utf8",
|
|
45
44
|
lang: "en",
|
|
@@ -83,7 +82,9 @@ export function loadProjectConfig(projectRoot: string) {
|
|
|
83
82
|
throw new Error(`Invalid project.json: ${errorMessage(error)}`, { cause: error });
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
// Deep merge with defaults
|
|
85
|
+
// Deep merge with defaults. The `...raw` spread carries every author-declared key through
|
|
86
|
+
// Untouched — including `extensions` and extension-contributed sections (`content`, ...), which
|
|
87
|
+
// Are opaque to the loader (specs/extensions.md §3.1).
|
|
87
88
|
const config: ResolvedProjectConfig = {
|
|
88
89
|
...DEFAULTS,
|
|
89
90
|
...raw,
|
|
@@ -111,9 +112,6 @@ export function loadProjectConfig(projectRoot: string) {
|
|
|
111
112
|
if (raw.imports) {
|
|
112
113
|
config.imports = raw.imports;
|
|
113
114
|
}
|
|
114
|
-
if (raw.contentTypes) {
|
|
115
|
-
config.contentTypes = raw.contentTypes;
|
|
116
|
-
}
|
|
117
115
|
|
|
118
116
|
// Validate adapter
|
|
119
117
|
const VALID_ADAPTERS = ["cloudflare-workers", "cloudflare-pages", "node", "bun"];
|
|
@@ -56,15 +56,47 @@ export default app
|
|
|
56
56
|
`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* One extension server mount to emit into the generated worker (specs/extensions.md §11). The
|
|
61
|
+
* module is imported statically (Workers cannot dynamic-import), the options are inlined JSON
|
|
62
|
+
* (identifiers only — never secrets), and every mount shares one context object, called in
|
|
63
|
+
* ascending `order`.
|
|
64
|
+
*/
|
|
65
|
+
export interface SiteMountSpec {
|
|
66
|
+
/** Export name of the mount class (the descriptor title, e.g. "Data"). */
|
|
67
|
+
className: string;
|
|
68
|
+
/** Bare module specifier the worker imports (`server.module`). */
|
|
69
|
+
module: string;
|
|
70
|
+
/** Route subtree the mount owns (`server.basePath`). */
|
|
71
|
+
basePath: string;
|
|
72
|
+
/** Mount order (`server.order`, ascending). */
|
|
73
|
+
order: number;
|
|
74
|
+
/** JSON-inlined mount options (project sections manifest etc.). */
|
|
75
|
+
options: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A connector provider class the worker imports and hands to mounts via `options.connectors`. */
|
|
79
|
+
export interface SiteConnectorSpec {
|
|
80
|
+
/** The `connector.provider` id (options.connectors key). */
|
|
81
|
+
provider: string;
|
|
82
|
+
/** Export name of the provider implementation (the descriptor title). */
|
|
83
|
+
className: string;
|
|
84
|
+
/** Bare module specifier (`connector.module`). */
|
|
85
|
+
module: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
/**
|
|
60
89
|
* Generate a bundled Hono server entry from pre-collected server entries across an entire site.
|
|
61
|
-
* Used when `build.adapter` is set in project.json. Returns null if no entries are
|
|
90
|
+
* Used when `build.adapter` is set in project.json. Returns null if no entries or mounts are
|
|
91
|
+
* provided.
|
|
62
92
|
*
|
|
63
93
|
* @param {{ exportName: string; src: string }[]} entries - Resolved server entries
|
|
64
94
|
* @param {object} [opts]
|
|
65
95
|
* @param {string} [opts.baseUrl] - Base path for server endpoints. Default is `'/_jx/server'`
|
|
66
96
|
* @param {string | null} [opts.adapter] - Deployment adapter: `"cloudflare-workers"`,
|
|
67
97
|
* `"cloudflare-pages"`, `"node"`, `"bun"`
|
|
98
|
+
* @param {SiteMountSpec[]} [opts.mounts] - Extension server mounts, already sorted by order
|
|
99
|
+
* @param {SiteConnectorSpec[]} [opts.connectors] - Connector classes used by the mounts
|
|
68
100
|
* @returns {string | null} Complete worker/server source string, or null
|
|
69
101
|
*/
|
|
70
102
|
export function compileSiteServer(
|
|
@@ -72,10 +104,12 @@ export function compileSiteServer(
|
|
|
72
104
|
opts: {
|
|
73
105
|
baseUrl?: string;
|
|
74
106
|
adapter?: string | null;
|
|
107
|
+
mounts?: SiteMountSpec[];
|
|
108
|
+
connectors?: SiteConnectorSpec[];
|
|
75
109
|
} = {},
|
|
76
110
|
) {
|
|
77
|
-
const { baseUrl = "/_jx/server", adapter = null } = opts;
|
|
78
|
-
if (entries.length === 0 && !adapter) {
|
|
111
|
+
const { baseUrl = "/_jx/server", adapter = null, mounts = [], connectors = [] } = opts;
|
|
112
|
+
if (entries.length === 0 && mounts.length === 0 && !adapter) {
|
|
79
113
|
return null;
|
|
80
114
|
}
|
|
81
115
|
|
|
@@ -83,6 +117,8 @@ export function compileSiteServer(
|
|
|
83
117
|
.map(({ exportName, src }) => `import { ${exportName} } from '${src}'`)
|
|
84
118
|
.join("\n");
|
|
85
119
|
|
|
120
|
+
const mountBlock = buildMountBlock(mounts, connectors);
|
|
121
|
+
|
|
86
122
|
const routes = entries.map(({ exportName }) => buildRoute(exportName, baseUrl)).join("\n");
|
|
87
123
|
|
|
88
124
|
// Workers static assets and Pages advanced mode (_worker.js) both route every request
|
|
@@ -94,15 +130,57 @@ export function compileSiteServer(
|
|
|
94
130
|
|
|
95
131
|
return `// Generated by @jxsuite/compiler — do not edit manually
|
|
96
132
|
import { Hono } from 'hono'
|
|
97
|
-
${imports}
|
|
133
|
+
${imports}${mountBlock.imports}
|
|
98
134
|
|
|
99
135
|
const app = new Hono()
|
|
100
|
-
${routes}
|
|
136
|
+
${mountBlock.routes}${routes}
|
|
101
137
|
${adapterBlock}
|
|
102
138
|
export default app
|
|
103
139
|
`;
|
|
104
140
|
}
|
|
105
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Emit the extension-mount section: static imports, one shared ctx, ordered `mount()` calls at
|
|
144
|
+
* module init (top-level await), and `app.all('<basePath>/*', …)` wrappers registered BEFORE the
|
|
145
|
+
* /_jx/server routes and the ASSETS fallthrough.
|
|
146
|
+
*
|
|
147
|
+
* @param {SiteMountSpec[]} mounts
|
|
148
|
+
* @param {SiteConnectorSpec[]} connectors
|
|
149
|
+
* @returns {{ imports: string; routes: string }}
|
|
150
|
+
*/
|
|
151
|
+
function buildMountBlock(mounts: SiteMountSpec[], connectors: SiteConnectorSpec[]) {
|
|
152
|
+
if (mounts.length === 0) {
|
|
153
|
+
return { imports: "", routes: "" };
|
|
154
|
+
}
|
|
155
|
+
const ordered = mounts.toSorted((a, b) => a.order - b.order);
|
|
156
|
+
|
|
157
|
+
const importLines: string[] = [];
|
|
158
|
+
for (const mount of ordered) {
|
|
159
|
+
importLines.push(`import { ${mount.className} } from '${mount.module}'`);
|
|
160
|
+
}
|
|
161
|
+
for (const connector of connectors) {
|
|
162
|
+
importLines.push(`import { ${connector.className} } from '${connector.module}'`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const lines: string[] = [
|
|
166
|
+
"// Extension server mounts (specs/extensions.md §11): one shared ctx, mounted in order.",
|
|
167
|
+
"const jxCtx = {}",
|
|
168
|
+
`const jxConnectors = { ${connectors
|
|
169
|
+
.map((c) => `${JSON.stringify(c.provider)}: ${c.className}`)
|
|
170
|
+
.join(", ")} }`,
|
|
171
|
+
];
|
|
172
|
+
for (const [index, mount] of ordered.entries()) {
|
|
173
|
+
const optionsJson = JSON.stringify(mount.options);
|
|
174
|
+
lines.push(
|
|
175
|
+
`const jxMount${index} = await ${mount.className}.mount({ ...${optionsJson}, connectors: jxConnectors }, jxCtx)`,
|
|
176
|
+
`app.all('${mount.basePath}/*', (c) => jxMount${index}(c.req.raw, c.env))`,
|
|
177
|
+
`app.all('${mount.basePath}', (c) => jxMount${index}(c.req.raw, c.env))`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { imports: `\n${importLines.join("\n")}`, routes: `${lines.join("\n")}\n` };
|
|
182
|
+
}
|
|
183
|
+
|
|
106
184
|
/** @param {string} exportName @param {string} baseUrl */
|
|
107
185
|
function buildRoute(exportName: string, baseUrl: string) {
|
|
108
186
|
return `
|