@junejs/server 0.1.2 → 0.2.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/assets/icon-latin.LICENSE.txt +92 -0
- package/assets/icon-latin.ttf +0 -0
- package/dist/adapter-DcZ-W1-7.d.ts +62 -0
- package/dist/adapter.d.ts +2 -56
- package/dist/adapter.js +62 -5
- package/dist/agent-compile.d.ts +38 -0
- package/dist/agent-compile.js +145 -0
- package/dist/agent-discover.d.ts +4 -4
- package/dist/agent-discover.js +30 -44
- package/dist/agent-durable.d.ts +94 -19
- package/dist/agent-durable.js +647 -32
- package/dist/agent-native.d.ts +44 -13
- package/dist/agent-native.js +264 -27
- package/dist/app.d.ts +5 -5
- package/dist/app.js +42 -10
- package/dist/blob.d.ts +4 -5
- package/dist/build.d.ts +8 -29
- package/dist/build.js +138 -289
- package/dist/client-bundle.d.ts +8 -9
- package/dist/client-bundle.js +1 -1
- package/dist/config-freeze-CyXvNqi2.d.ts +17 -0
- package/dist/config-freeze.d.ts +2 -0
- package/dist/config-freeze.js +54 -0
- package/dist/config-loader.d.ts +3 -4
- package/dist/connection-auth.d.ts +38 -0
- package/dist/connection-auth.js +31 -0
- package/dist/content-freeze-CyiW2fRK.d.ts +4 -0
- package/dist/content-freeze.d.ts +2 -0
- package/dist/content-freeze.js +83 -0
- package/dist/content.d.ts +22 -9
- package/dist/content.js +43 -3
- package/dist/core-version.d.ts +4 -0
- package/dist/core-version.js +12 -0
- package/dist/css-modules-ambient.d.ts +2 -1
- package/dist/css-modules.d.ts +10 -11
- package/dist/css.d.ts +9 -10
- package/dist/d1-migrate.d.ts +6 -7
- package/dist/d1-migrate.js +3 -2
- package/dist/db.d.ts +2 -2
- package/dist/deploy.d.ts +3 -4
- package/dist/deploy.js +3 -2
- package/dist/dev-reload.d.ts +3 -4
- package/dist/dev.d.ts +8 -5
- package/dist/dev.js +40 -9
- package/dist/favicon.d.ts +37 -0
- package/dist/favicon.js +248 -0
- package/dist/host.d.ts +6 -5
- package/dist/host.js +2 -1
- package/dist/icon-letter.d.ts +3 -0
- package/dist/icon-letter.js +8 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +9 -3
- package/dist/instrumentation.d.ts +2 -3
- package/dist/instrumentation.js +2 -1
- package/dist/island-registry.d.ts +6 -7
- package/dist/isolate-local.d.ts +5 -0
- package/dist/isolate-local.js +12 -0
- package/dist/kv.d.ts +3 -4
- package/dist/llms-links.d.ts +9 -0
- package/dist/llms-links.js +65 -0
- package/dist/manifest-CwTT1zVs.d.ts +11 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +113 -0
- package/dist/negotiate.d.ts +4 -3
- package/dist/negotiate.js +24 -14
- package/dist/{pipeline-CCp9idQ4.d.ts → pipeline-BHUjBO0G.d.ts} +13 -1
- package/dist/pipeline.d.ts +2 -2
- package/dist/pipeline.js +286 -35
- package/dist/resources.d.ts +5 -6
- package/dist/route-rank.d.ts +10 -0
- package/dist/route-rank.js +38 -0
- package/dist/route-scan-BMbBzPIB.d.ts +20 -0
- package/dist/route-scan.d.ts +2 -0
- package/dist/route-scan.js +100 -0
- package/dist/router.d.ts +12 -13
- package/dist/router.js +58 -44
- package/dist/rsc-build.d.ts +6 -7
- package/dist/rsc-bundle.d.ts +6 -7
- package/dist/rsc-manifest.d.ts +5 -6
- package/dist/rsc-runtime/flight-render-routes.d.ts +3 -4
- package/dist/rsc-runtime/flight-render.d.ts +2 -3
- package/dist/rsc-runtime/flight-to-html.d.ts +2 -3
- package/dist/rsc-runtime/flight-to-html.js +3 -2
- package/dist/rsc-runtime/rsc-worker.js +11 -8
- package/dist/rsc-runtime/rsdw-shims.d.ts +2 -1
- package/dist/segment.d.ts +4 -5
- package/dist/sqlite-driver.d.ts +11 -11
- package/dist/sqlite-driver.js +17 -4
- package/dist/static-files.d.ts +4 -5
- package/dist/tsconfig-jsx.d.ts +4 -4
- package/dist/tsconfig-jsx.js +1 -1
- package/dist/turn-events.d.ts +4 -0
- package/dist/turn-events.js +46 -0
- package/dist/web-manifest.d.ts +5 -0
- package/dist/web-manifest.js +42 -0
- package/dist/worker-2qbQjj2c.d.ts +64 -0
- package/dist/worker.d.ts +2 -56
- package/dist/worker.js +121 -103
- package/package.json +22 -9
- package/src/adapter.ts +84 -3
- package/src/agent-compile.ts +271 -0
- package/src/agent-discover.ts +77 -63
- package/src/agent-durable.ts +998 -30
- package/src/agent-native.ts +370 -35
- package/src/app.ts +75 -6
- package/src/build.ts +237 -444
- package/src/config-freeze.ts +109 -0
- package/src/connection-auth.ts +101 -0
- package/src/content-freeze.ts +151 -0
- package/src/content.ts +55 -4
- package/src/core-version.ts +34 -0
- package/src/dev.ts +41 -8
- package/src/favicon.ts +325 -0
- package/src/host.ts +4 -3
- package/src/icon-letter.ts +14 -0
- package/src/index.ts +22 -0
- package/src/isolate-local.ts +33 -0
- package/src/llms-links.ts +96 -0
- package/src/manifest.ts +130 -0
- package/src/negotiate.ts +39 -15
- package/src/pipeline.ts +494 -44
- package/src/route-rank.ts +60 -0
- package/src/route-scan.ts +161 -0
- package/src/router.ts +75 -58
- package/src/rsc-runtime/rsc-worker.tsx +5 -4
- package/src/sqlite-driver.ts +36 -6
- package/src/tsconfig-jsx.ts +2 -2
- package/src/turn-events.ts +43 -0
- package/src/web-manifest.ts +38 -0
- package/src/worker.ts +197 -117
package/src/build.ts
CHANGED
|
@@ -6,45 +6,53 @@
|
|
|
6
6
|
// SAME pipeline as dev (pipeline.ts), so its surfaces are byte-equivalent —
|
|
7
7
|
// proven by test/parity.test.ts, not hoped for.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
9
|
+
// The build's separable stages live in sibling modules (re-exported here so
|
|
10
|
+
// consumers keep one import root):
|
|
11
|
+
// route-scan.ts — app/ + .june/routes/ discovery and merge (scanAppRoutes)
|
|
12
|
+
// content-freeze.ts — content/ → app/_content.ts (generateContent)
|
|
13
|
+
// config-freeze.ts — june.config.ts → serializable worker bits (freezeConfig)
|
|
14
|
+
// manifest.ts — the in-process freeze (buildManifest; parity + prerender)
|
|
15
|
+
// This file keeps the full-build orchestration: juneBuild = content freeze +
|
|
16
|
+
// generated entry + Rolldown bundle (workerd conditions, binary externals) +
|
|
17
|
+
// prerender-through-the-worker + wrangler config.
|
|
15
18
|
//
|
|
16
19
|
// REMINDER #4: nothing in the worker graph may statically import node:*. The
|
|
17
20
|
// content freeze (content/*.md → app/_content.ts) is what removes fs from the
|
|
18
21
|
// dynamic route's graph; the worker reads frozen data, never the filesystem.
|
|
19
22
|
|
|
20
|
-
import { execFileSync } from "node:child_process";
|
|
21
23
|
import { createHash } from "node:crypto";
|
|
22
24
|
import { existsSync, lstatSync } from "node:fs";
|
|
23
|
-
import { copyFile, mkdir, readdir,
|
|
25
|
+
import { copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
24
26
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
25
|
-
import { pathToFileURL } from "node:url";
|
|
26
27
|
|
|
27
|
-
import {
|
|
28
|
-
import { resolveAgent, resolveClientRouter, resolveSpeculationRules } from "@junejs/core/config";
|
|
29
|
-
import type { ContentSource, JuneConfig } from "@junejs/core/config";
|
|
28
|
+
import { loadJuneConfig } from "./config-loader";
|
|
30
29
|
import { buildLinkHeader } from "@junejs/core/discovery";
|
|
30
|
+
import { PRERENDER_ORIGIN } from "@junejs/core/document";
|
|
31
31
|
import { localeHref } from "@junejs/core/i18n";
|
|
32
|
-
import {
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
32
|
+
import type { BrandedRoute } from "@junejs/core/route";
|
|
33
|
+
import type { ResourcePlan } from "./adapter";
|
|
34
|
+
import { generateAgentModule } from "./agent-compile";
|
|
35
|
+
import { freezeConfig, resolveDeployAdapter, workerName } from "./config-freeze";
|
|
36
|
+
import { generateContent } from "./content-freeze";
|
|
37
|
+
import { GENERATED_ICONS, googleFontFetcher, publicFileCheck, renderIcons, resolveIcons } from "./favicon";
|
|
38
|
+
import { createWorker } from "./worker";
|
|
39
|
+
import { buildManifest, importLayout, type ImportedLayout } from "./manifest";
|
|
37
40
|
import { findMiddlewareFile } from "./router";
|
|
41
|
+
import { scanAppRoutes } from "./route-scan";
|
|
38
42
|
import { resolveBoundary } from "./segment";
|
|
39
|
-
import
|
|
40
|
-
import { findClientEntry, bundleClientToFile, CLIENT_SCRIPT_URL } from "./client-bundle";
|
|
43
|
+
import { findClientEntry, bundleClientToFile } from "./client-bundle";
|
|
41
44
|
import { RESERVED_PREFIX } from "./static-files";
|
|
42
45
|
import { jsxTransform } from "./tsconfig-jsx";
|
|
43
46
|
import { generateIslandRegistry } from "./island-registry";
|
|
44
47
|
import { buildRsc, findRscRoutes } from "./rsc-build";
|
|
45
|
-
import { cssTargets,
|
|
48
|
+
import { cssTargets, minifyCss, processCss } from "./css";
|
|
46
49
|
import { buildModuleCss, rolldownCssModulesPlugin, registerCssModules } from "./css-modules";
|
|
47
50
|
|
|
51
|
+
export { scanRoutes, scanAppRoutes, type RouteEntry } from "./route-scan";
|
|
52
|
+
export { generateContent } from "./content-freeze";
|
|
53
|
+
export { freezeConfig, normalizeBase, resolveDeployAdapter, workerName } from "./config-freeze";
|
|
54
|
+
export { buildManifest } from "./manifest";
|
|
55
|
+
|
|
48
56
|
export type BuildResult = {
|
|
49
57
|
outFile: string;
|
|
50
58
|
routes: string[];
|
|
@@ -53,409 +61,12 @@ export type BuildResult = {
|
|
|
53
61
|
prerendered: string[];
|
|
54
62
|
};
|
|
55
63
|
|
|
56
|
-
// The segment layout CHAIN root→leaf: every directory level (route groups
|
|
57
|
-
// included) may contribute a layout.* that wraps routes below it.
|
|
58
|
-
type RouteEntry = {
|
|
59
|
-
path: string;
|
|
60
|
-
file: string;
|
|
61
|
-
dynamic: boolean;
|
|
62
|
-
resource?: boolean; // a route.* resource route (raw-Response handler), not a page
|
|
63
|
-
layouts: string[];
|
|
64
|
-
loading?: string; // nearest loading.tsx up the tree → streaming Suspense fallback
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const PAGE_BASENAMES = new Set(["page", "index"]);
|
|
68
|
-
const ROUTE_EXTS = [".tsx", ".jsx", ".ts", ".js"];
|
|
69
|
-
|
|
70
|
-
const isRouteGroup = (name: string) => /^\(.+\)$/.test(name);
|
|
71
|
-
|
|
72
64
|
// Bun built-ins (`bun`, `bun:sqlite`, …) exist only at the Bun runtime and must never enter the
|
|
73
65
|
// workerd graph. Marking them external keeps rolldown from constant-folding the `const x = "bun";
|
|
74
66
|
// import(x)` runtime guard (in @junejs/core's cache.ts) and warning UNRESOLVED_IMPORT. Exported so
|
|
75
67
|
// the build keeps externalizing them — see test/build-externals.test.ts.
|
|
76
68
|
export const isBunSpecifier = (id: string): boolean => id === "bun" || id.startsWith("bun:");
|
|
77
69
|
|
|
78
|
-
// Resolve the deploy adapter from config. An explicit `adapter` INSTANCE wins; otherwise the
|
|
79
|
-
// `target` NAME selects the matching built-in — so a DECLARATIVE config (e.g. kura.toml, which
|
|
80
|
-
// can't express a `vercel()` call) can pick any target by string, not just "static". "workers" is
|
|
81
|
-
// the default. Kept in lockstep with deploy.ts's own target→deployer switch so a build for one
|
|
82
|
-
// target is never deployed as another. vercel()/deno() take their opts (runtime/regions, org/app)
|
|
83
|
-
// which JuneConfig.deploy doesn't carry yet, so they use defaults here.
|
|
84
|
-
export function resolveDeployAdapter(deploy: JuneConfig["deploy"]): JuneAdapter {
|
|
85
|
-
if (deploy?.adapter) return deploy.adapter as JuneAdapter;
|
|
86
|
-
switch (deploy?.target) {
|
|
87
|
-
case "static":
|
|
88
|
-
return staticSite();
|
|
89
|
-
case "vercel":
|
|
90
|
-
return vercel();
|
|
91
|
-
case "deno":
|
|
92
|
-
return deno();
|
|
93
|
-
default:
|
|
94
|
-
return workers({ name: deploy?.name, domain: deploy?.domain });
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function segmentFile(dir: string, base: string): string | undefined {
|
|
99
|
-
return ROUTE_EXTS.map((e) => join(dir, `${base}${e}`)).find(existsSync);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Walk app/ for page.* files → route paths (mirrors router.ts conventions:
|
|
103
|
-
// route groups vanish from URLs, `_`-prefixed entries are private), carrying the
|
|
104
|
-
// layout chain accumulated from each directory level.
|
|
105
|
-
export async function scanRoutes(
|
|
106
|
-
appDir: string,
|
|
107
|
-
dir = appDir,
|
|
108
|
-
layouts: string[] = [],
|
|
109
|
-
out: RouteEntry[] = [],
|
|
110
|
-
loading?: string,
|
|
111
|
-
): Promise<RouteEntry[]> {
|
|
112
|
-
const ownLayout = segmentFile(dir, "layout");
|
|
113
|
-
const chain = ownLayout ? [...layouts, ownLayout] : layouts;
|
|
114
|
-
const nearestLoading = segmentFile(dir, "loading") ?? loading;
|
|
115
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
116
|
-
for (const e of entries) {
|
|
117
|
-
if (e.name.startsWith("_") || e.name.startsWith(".")) continue;
|
|
118
|
-
const full = join(dir, e.name);
|
|
119
|
-
if (e.isDirectory()) {
|
|
120
|
-
await scanRoutes(appDir, full, chain, out, nearestLoading);
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
const ext = e.name.match(/\.[^.]+$/)?.[0] ?? "";
|
|
124
|
-
if (!ROUTE_EXTS.includes(ext)) continue;
|
|
125
|
-
const base = basename(e.name, ext);
|
|
126
|
-
const resource = base === "route";
|
|
127
|
-
if (!PAGE_BASENAMES.has(base) && !resource) continue;
|
|
128
|
-
const relDir = relative(appDir, dir);
|
|
129
|
-
const segments = relDir === "" ? [] : relDir.split(sep).filter((s) => !isRouteGroup(s));
|
|
130
|
-
const path = "/" + segments.join("/");
|
|
131
|
-
out.push({
|
|
132
|
-
path: path === "/" ? "/" : path,
|
|
133
|
-
file: full,
|
|
134
|
-
dynamic: /\[.+\]/.test(path),
|
|
135
|
-
resource,
|
|
136
|
-
layouts: chain,
|
|
137
|
-
loading: nearestLoading,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return out;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// content/<collection>/*.md → app/_content.ts (the build-time content manifest).
|
|
144
|
-
// This is the FREEZE that removes node:fs from the worker graph: routes import
|
|
145
|
-
// frozen entries from ./_content instead of reading the filesystem at request.
|
|
146
|
-
//
|
|
147
|
-
// Extra sources (config `content.sources`) merge dirs OUTSIDE `content/` into named
|
|
148
|
-
// collections — the docs-as-code seam (a repo's own `docs/` feeds the site directly).
|
|
149
|
-
// Their dirs resolve against the app root, so "../docs" reaches a sibling directory.
|
|
150
|
-
//
|
|
151
|
-
// Locale buckets are DECLARED, not guessed: only dirs named in config i18n
|
|
152
|
-
// (defaultLocale + locales keys) split off as locale mirrors. Guessing by shape
|
|
153
|
-
// (the old BCP-47 regex) silently swallowed any 2–3-letter folder — `cli/`,
|
|
154
|
-
// `sdk/`, `api/`, `faq/` all read as locales and vanished from the default set.
|
|
155
|
-
// No i18n config ⇒ NO buckets (an undeclared locale is not a locale). The regex
|
|
156
|
-
// remains only as the fallback when the config itself cannot be loaded.
|
|
157
|
-
export async function generateContent(appRoot: string): Promise<string[]> {
|
|
158
|
-
const contentDir = join(appRoot, "content");
|
|
159
|
-
// app/ is the output dir for _content.ts (and the seed). Ensure it exists so the now-always
|
|
160
|
-
// write can't throw an opaque ENOENT when called standalone (e.g. `june gen`); the full build's
|
|
161
|
-
// own "no app/ directory" check runs earlier, so this never masks that clearer error.
|
|
162
|
-
await mkdir(join(appRoot, "app"), { recursive: true });
|
|
163
|
-
const emit = async (sources: ContentSource[], knownLocales: readonly string[] | undefined): Promise<string[]> => {
|
|
164
|
-
// Emission (incl. the per-locale layout and source merging) lives in ./content so it's
|
|
165
|
-
// pure and unit-testable; this stays the thin fs wrapper.
|
|
166
|
-
const { code, names } = generateContentModule(contentDir, knownLocales, sources);
|
|
167
|
-
// Always write, even with zero collections: `code` still carries the canonical ContentEntry
|
|
168
|
-
// type, which is the single source of truth the bootstrap seed appends its stubs against (an
|
|
169
|
-
// app with no local content/ — docs-as-code — needs this valid empty module to exist).
|
|
170
|
-
await writeFile(join(appRoot, "app", "_content.ts"), code);
|
|
171
|
-
return names;
|
|
172
|
-
};
|
|
173
|
-
const resolveSources = (sources: ContentSource[]): ContentSource[] =>
|
|
174
|
-
sources.map((s) => ({ ...s, dir: resolve(appRoot, s.dir) }));
|
|
175
|
-
const declaredLocales = (config: JuneConfig): string[] =>
|
|
176
|
-
config.i18n ? [...new Set([config.i18n.defaultLocale, ...Object.keys(config.i18n.locales)])] : [];
|
|
177
|
-
// Learn sources + locales from june.config.ts — TOLERANTLY. A wrapper-generated config (e.g.
|
|
178
|
-
// Kura's) imports app/_content.ts, which does not exist before the FIRST freeze. So on a failed
|
|
179
|
-
// config load: generate a default scan to create that import's target, then re-probe the config
|
|
180
|
-
// and regenerate with the real sources/locales. Self-healing, no bootstrap flag. Content errors
|
|
181
|
-
// from emit() (slug collision, missing source dir) stay loud — only the CONFIG LOAD is tolerated.
|
|
182
|
-
let config: JuneConfig | null = null;
|
|
183
|
-
try {
|
|
184
|
-
config = await loadJuneConfig(appRoot);
|
|
185
|
-
} catch {
|
|
186
|
-
/* two-pass below */
|
|
187
|
-
}
|
|
188
|
-
if (config) return emit(resolveSources(config.content?.sources ?? []), declaredLocales(config));
|
|
189
|
-
// Pass 1 (throwaway when the probe succeeds): legacy regex locale detection, since the
|
|
190
|
-
// declared set is unknowable without the config. emit() writes app/_content.ts with the
|
|
191
|
-
// canonical ContentEntry type plus a `const` per collection FOUND — but a docs-as-code app
|
|
192
|
-
// keeps ALL content in external content.sources with NO local content/, so it finds zero
|
|
193
|
-
// collections and thus emits no EXPORTS (no `DOCS`). seedContentImports then appends stubs for
|
|
194
|
-
// the exact names the config imports (e.g. Kura's `import { DOCS }`), so the re-probe can load.
|
|
195
|
-
const names = await emit([], undefined);
|
|
196
|
-
await seedContentImports(appRoot);
|
|
197
|
-
const probed = probeConfigFresh(appRoot);
|
|
198
|
-
if (probed === null) {
|
|
199
|
-
console.warn("[june gen] june.config.ts failed to load — content.sources/i18n locales (if any) not applied");
|
|
200
|
-
return names;
|
|
201
|
-
}
|
|
202
|
-
return emit(resolveSources(probed.sources), probed.locales);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Bootstrap seed: ensure app/_content.ts exports every name the config imports from it, so the
|
|
206
|
-
// re-probe's config load resolves BEFORE the first real freeze. A docs-as-code app (content only
|
|
207
|
-
// in external content.sources, no local content/) leaves Pass 1's default scan empty — nothing
|
|
208
|
-
// seeds the collections — so here we append stubs for the EXACT named imports the config takes
|
|
209
|
-
// from the app/_content module (a bare `import { DOCS }` of it → `export const DOCS = []`). The
|
|
210
|
-
// caller's emit() already wrote the module's canonical ContentEntry type (even when empty), so the
|
|
211
|
-
// stubs type against that single source of truth. Overwritten by the real freeze that follows a
|
|
212
|
-
// successful probe; a no-op once content exists.
|
|
213
|
-
async function seedContentImports(appRoot: string): Promise<void> {
|
|
214
|
-
const cfgPath = findJuneConfigPath(appRoot);
|
|
215
|
-
if (!cfgPath) return;
|
|
216
|
-
const contentFile = join(appRoot, "app", "_content.ts");
|
|
217
|
-
const current = existsSync(contentFile) ? await readFile(contentFile, "utf8") : "";
|
|
218
|
-
const cfgSrc = await readFile(cfgPath, "utf8");
|
|
219
|
-
// Only stub valid JS identifiers — a name carrying comments/other tokens (rare, but valid TS)
|
|
220
|
-
// must never reach `new RegExp(...)` (would throw) or the emitted stub (would be invalid TS).
|
|
221
|
-
const isIdent = (s: string): boolean => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
222
|
-
// A Set so a name matched more than once (e.g. a comment mentioning the import + the real one)
|
|
223
|
-
// yields ONE stub — a duplicate `export const <name>` would be invalid TS. Also skip names the
|
|
224
|
-
// module already exports.
|
|
225
|
-
const seen = new Set<string>();
|
|
226
|
-
const stubs: string[] = [];
|
|
227
|
-
// Each named `import { A, B as C }` the config takes from the app/_content module → stub each.
|
|
228
|
-
// The `(?:\.\w+)?` tolerates an explicit extension (`app/_content.ts`/`.js`/`.mjs`), which
|
|
229
|
-
// NodeNext / verbatimModuleSyntax configs require — else the import goes undetected and unstubbed.
|
|
230
|
-
for (const m of cfgSrc.matchAll(/import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*["'][^"']*app\/_content(?:\.\w+)?["']/g)) {
|
|
231
|
-
// Strip block + line comments from the specifier list BEFORE splitting: a valid
|
|
232
|
-
// `{ DOCS /* keep */ }` must still yield the identifier `DOCS` (not be dropped), and a comment
|
|
233
|
-
// could otherwise carry a `,` that breaks the split. Then drop `type` markers per specifier.
|
|
234
|
-
const inner = (m[1] ?? "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
235
|
-
for (const part of inner.split(",")) {
|
|
236
|
-
const name = part.replace(/\btype\b/g, "").split(/\s+as\s+/).pop()?.trim();
|
|
237
|
-
if (!name || !isIdent(name) || seen.has(name)) continue;
|
|
238
|
-
seen.add(name);
|
|
239
|
-
// Escape before interpolating: a valid identifier may contain `$`, a regex metachar (an
|
|
240
|
-
// anchor), which would corrupt the already-exported check. The stub below uses the raw name.
|
|
241
|
-
const reName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
|
-
if (!new RegExp(`export (?:const|function|type) ${reName}\\b`).test(current)) {
|
|
243
|
-
stubs.push(`export const ${name}: ContentEntry[] = [];`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (stubs.length) await writeFile(contentFile, current + stubs.join("\n") + "\n");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Re-probe ONLY the content-relevant config (sources + declared i18n locales) in a FRESH
|
|
251
|
-
// subprocess. The bootstrap retry can't re-import in-process: a failed ESM load is cached as
|
|
252
|
-
// errored, and Bun also caches the failed RESOLUTION of the config's own imports — so a
|
|
253
|
-
// same-process retry (even cache-busted) re-rejects after the missing app/_content.ts appears.
|
|
254
|
-
// A child process has a clean module map, and both fields are plain JSON, so they survive the
|
|
255
|
-
// pipe. Returns null when the config is genuinely broken.
|
|
256
|
-
function probeConfigFresh(appRoot: string): { sources: ContentSource[]; locales: string[] } | null {
|
|
257
|
-
const path = findJuneConfigPath(appRoot);
|
|
258
|
-
if (!path) return { sources: [], locales: [] };
|
|
259
|
-
// Markers isolate the JSON from anything the config prints at import time. `.then` (not TLA)
|
|
260
|
-
// so the eval works as CJS under both `bun -e` and `node -e`; execArgv carries loader flags
|
|
261
|
-
// (e.g. --experimental-strip-types) so the child can read the same TS config the parent does.
|
|
262
|
-
const code =
|
|
263
|
-
`import(${JSON.stringify(pathToFileURL(path).href)}).then(` +
|
|
264
|
-
`(m) => { const c = m.default ?? {}; process.stdout.write("\\n__JUNE_CONTENT__" + JSON.stringify({ ` +
|
|
265
|
-
`sources: c.content?.sources ?? [], ` +
|
|
266
|
-
`locales: c.i18n ? [...new Set([c.i18n.defaultLocale, ...Object.keys(c.i18n.locales ?? {})])] : [] ` +
|
|
267
|
-
`}) + "__END__"); }, () => process.exit(42));`;
|
|
268
|
-
try {
|
|
269
|
-
const out = execFileSync(process.execPath, [...process.execArgv, "-e", code], {
|
|
270
|
-
encoding: "utf8",
|
|
271
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
272
|
-
});
|
|
273
|
-
const m = out.match(/__JUNE_CONTENT__(.*?)__END__/s);
|
|
274
|
-
return m ? (JSON.parse(m[1]!) as { sources: ContentSource[]; locales: string[] }) : null;
|
|
275
|
-
} catch {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Freeze june.config.ts → the serializable bits the worker inlines.
|
|
281
|
-
export async function freezeConfig(appRoot: string): Promise<{
|
|
282
|
-
document: DocumentConfig;
|
|
283
|
-
agent: WorkerManifest["agent"];
|
|
284
|
-
i18n: WorkerManifest["i18n"];
|
|
285
|
-
earlyHints: string[];
|
|
286
|
-
buildExternal: string[];
|
|
287
|
-
}> {
|
|
288
|
-
const cfg = await loadJuneConfig(appRoot);
|
|
289
|
-
// An app with a client entry gets the islands runtime URL frozen into its
|
|
290
|
-
// document. Detected HERE (not just in juneBuild) so the prerender path —
|
|
291
|
-
// which re-freezes through buildManifest — sets the SAME clientScript, keeping
|
|
292
|
-
// prerendered pages byte-equivalent to the live worker (parity).
|
|
293
|
-
// Client entry: app/_client.* wins; fall back to .june/routes/_client.* (framework slot).
|
|
294
|
-
const hasClient =
|
|
295
|
-
findClientEntry(join(appRoot, "app")) !== undefined ||
|
|
296
|
-
findClientEntry(join(appRoot, ".june", "routes")) !== undefined;
|
|
297
|
-
const hasCss = findGlobalCss(join(appRoot, "app")) !== null;
|
|
298
|
-
return {
|
|
299
|
-
document: {
|
|
300
|
-
site: cfg.site ?? {},
|
|
301
|
-
speculationRules: resolveSpeculationRules(cfg.speculation ?? undefined),
|
|
302
|
-
speculationDelivery: "inline",
|
|
303
|
-
viewTransitions: cfg.viewTransitions ?? true,
|
|
304
|
-
// Default the baseline reset OFF when the app uses Tailwind (its Preflight is the reset).
|
|
305
|
-
cssReset: cfg.cssReset ?? !globalCssUsesTailwind(join(appRoot, "app")),
|
|
306
|
-
clientRouter: resolveClientRouter(cfg.clientRouter),
|
|
307
|
-
clientScript: hasClient ? CLIENT_SCRIPT_URL : null,
|
|
308
|
-
styles: hasCss ? STYLES_URL : null,
|
|
309
|
-
// Deploy subpath (JuneConfig.basePath). Frozen so the document prefixes its
|
|
310
|
-
// asset URLs; the prerender path re-freezes through buildManifest and gets the
|
|
311
|
-
// same value, keeping static pages' asset links correct under the subpath.
|
|
312
|
-
basePath: normalizeBase(cfg.basePath),
|
|
313
|
-
},
|
|
314
|
-
agent: resolveAgent(cfg.agent),
|
|
315
|
-
// Pass i18n through as-is: the in-process buildManifest keeps a resolveLocale
|
|
316
|
-
// hook (parity test), and the codegen JSON.stringify below drops the function
|
|
317
|
-
// (worker hook support is the codegen pass — see the manifest field comment).
|
|
318
|
-
i18n: cfg.i18n,
|
|
319
|
-
earlyHints: cfg.earlyHints ?? [],
|
|
320
|
-
buildExternal: cfg.build?.external ?? [],
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// A deploy basePath is stored with a leading slash and no trailing slash; empty
|
|
325
|
-
// ("" — the default) means a root deploy. So "/openab/docs/" → "/openab/docs",
|
|
326
|
-
// "openab" → "/openab", undefined → "".
|
|
327
|
-
export function normalizeBase(base?: string): string {
|
|
328
|
-
if (!base) return "";
|
|
329
|
-
const b = base.startsWith("/") ? base : `/${base}`;
|
|
330
|
-
return b.endsWith("/") ? b.slice(0, -1) : b;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
type ImportedLayout = { component: LayoutComponent; boundary: boolean };
|
|
334
|
-
async function importLayout(file: string): Promise<ImportedLayout | null> {
|
|
335
|
-
const mod = (await import(pathToFileURL(file).href)) as {
|
|
336
|
-
default?: LayoutComponent;
|
|
337
|
-
segmentBoundary?: unknown;
|
|
338
|
-
};
|
|
339
|
-
return typeof mod.default === "function"
|
|
340
|
-
? { component: mod.default, boundary: mod.segmentBoundary === true }
|
|
341
|
-
: null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// A wrangler-valid worker name from a package name or directory name. Wrangler requires lowercase
|
|
345
|
-
// [a-z0-9-] not starting/ending with a dash — so a SCOPED package name (`@scope/pkg`) must lose its
|
|
346
|
-
// scope (else the `@` sanitizes to a leading dash and wrangler rejects the config). Drop the scope,
|
|
347
|
-
// lowercase, collapse non-alphanumerics to single dashes, trim edge dashes; fall back to "app" if
|
|
348
|
-
// nothing survives (e.g. an all-punctuation name). Exported for tests.
|
|
349
|
-
export function workerName(raw: string): string {
|
|
350
|
-
return (
|
|
351
|
-
raw
|
|
352
|
-
.replace(/^@[^/]+\//, "")
|
|
353
|
-
.toLowerCase()
|
|
354
|
-
.replace(/[^a-z0-9-]+/g, "-")
|
|
355
|
-
.replace(/^-+|-+$/g, "") || "app"
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// The FREEZE, in-process: import route modules + layouts, build the manifest a
|
|
360
|
-
// createWorker() can run immediately. Used by prerender and by the parity test
|
|
361
|
-
// (its render path is identical to the Rolldown-bundled worker).
|
|
362
|
-
export async function buildManifest(appRoot: string): Promise<WorkerManifest> {
|
|
363
|
-
const appDir = join(appRoot, "app");
|
|
364
|
-
const juneRoutesDir = join(appRoot, ".june", "routes");
|
|
365
|
-
const frozen = await freezeConfig(appRoot);
|
|
366
|
-
|
|
367
|
-
// Merge routes: app/ takes priority over .june/routes/ (app/ is the escape hatch).
|
|
368
|
-
// .june/routes/ is the convention slot for framework-generated routes (e.g. kura
|
|
369
|
-
// writes its docs/search/og routes there so the user never manages boilerplate).
|
|
370
|
-
const appRoutes = await scanRoutes(appDir);
|
|
371
|
-
const frameworkRoutes = existsSync(juneRoutesDir) ? await scanRoutes(juneRoutesDir) : [];
|
|
372
|
-
const appPaths = new Set(appRoutes.map((r) => r.path));
|
|
373
|
-
const scanned = [
|
|
374
|
-
...appRoutes,
|
|
375
|
-
...frameworkRoutes.filter((r) => !appPaths.has(r.path)),
|
|
376
|
-
].sort((a, b) => a.path.localeCompare(b.path));
|
|
377
|
-
|
|
378
|
-
const layoutCache = new Map<string, ImportedLayout | null>();
|
|
379
|
-
const loadCached = async (f: string): Promise<ImportedLayout | null> => {
|
|
380
|
-
if (!layoutCache.has(f)) layoutCache.set(f, await importLayout(f));
|
|
381
|
-
return layoutCache.get(f) ?? null;
|
|
382
|
-
};
|
|
383
|
-
// The chain (root→leaf) + boundary index + shell key, via the SHARED resolver
|
|
384
|
-
// (the one place the deepest-wins rule lives), so the frozen manifest and the
|
|
385
|
-
// dev resolver can't drift — the parity contract.
|
|
386
|
-
const componentsFor = async (
|
|
387
|
-
files: string[],
|
|
388
|
-
): Promise<{ chain: LayoutComponent[]; boundaryIndex: number | null; key: string | null }> => {
|
|
389
|
-
const items = [];
|
|
390
|
-
for (const f of files) {
|
|
391
|
-
const c = await loadCached(f);
|
|
392
|
-
items.push({ file: f, entry: c?.component ?? null, boundary: !!c?.boundary });
|
|
393
|
-
}
|
|
394
|
-
return resolveBoundary(items);
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const loadingCache = new Map<string, LoadingComponent | null>();
|
|
398
|
-
const loadingFor = async (file?: string): Promise<LoadingComponent | undefined> => {
|
|
399
|
-
if (!file) return undefined;
|
|
400
|
-
if (!loadingCache.has(file)) {
|
|
401
|
-
const loaded = await importLayout(file);
|
|
402
|
-
loadingCache.set(file, loaded ? (loaded.component as LoadingComponent) : null);
|
|
403
|
-
}
|
|
404
|
-
return loadingCache.get(file) ?? undefined;
|
|
405
|
-
};
|
|
406
|
-
|
|
407
|
-
const routes: Record<string, BrandedRoute> = {};
|
|
408
|
-
const dynamicRoutes: Array<{ pattern: string; def: BrandedRoute }> = [];
|
|
409
|
-
const resourceRoutes: Array<{ pattern: string; handler: ResourceHandler }> = [];
|
|
410
|
-
const layoutChains: Record<string, LayoutComponent[]> = {};
|
|
411
|
-
const layoutBoundaries: Record<string, { index: number; key: string }> = {};
|
|
412
|
-
const loadings: Record<string, LoadingComponent> = {};
|
|
413
|
-
|
|
414
|
-
for (const r of scanned) {
|
|
415
|
-
const mod = await import(pathToFileURL(r.file).href);
|
|
416
|
-
// Resource route (route.*): the default export is the Response handler.
|
|
417
|
-
if (r.resource) {
|
|
418
|
-
const handler = (mod as { default?: unknown }).default;
|
|
419
|
-
if (typeof handler === "function") resourceRoutes.push({ pattern: r.path, handler: handler as ResourceHandler });
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
const def = routeFromModule(mod);
|
|
423
|
-
if (!def) continue;
|
|
424
|
-
const { chain, boundaryIndex, key } = await componentsFor(r.layouts);
|
|
425
|
-
const loading = await loadingFor(r.loading);
|
|
426
|
-
if (r.dynamic) {
|
|
427
|
-
dynamicRoutes.push({ pattern: r.path, def });
|
|
428
|
-
layoutChains[r.path] = chain;
|
|
429
|
-
} else {
|
|
430
|
-
routes[r.path] = def;
|
|
431
|
-
layoutChains[r.path] = chain;
|
|
432
|
-
}
|
|
433
|
-
if (boundaryIndex !== null && key !== null) layoutBoundaries[r.path] = { index: boundaryIndex, key };
|
|
434
|
-
if (loading) loadings[r.path] = loading;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
let extra: ExtraHandler | undefined;
|
|
438
|
-
const extraFile = findMiddlewareFile(appDir);
|
|
439
|
-
if (extraFile) {
|
|
440
|
-
const mod = (await import(pathToFileURL(extraFile).href)) as { default?: unknown };
|
|
441
|
-
if (typeof mod.default === "function") extra = mod.default as ExtraHandler;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
routes,
|
|
446
|
-
dynamicRoutes,
|
|
447
|
-
resourceRoutes,
|
|
448
|
-
layoutChains,
|
|
449
|
-
layoutBoundaries,
|
|
450
|
-
loadings,
|
|
451
|
-
document: frozen.document,
|
|
452
|
-
agent: frozen.agent,
|
|
453
|
-
i18n: frozen.i18n,
|
|
454
|
-
earlyHints: frozen.earlyHints,
|
|
455
|
-
extra,
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
70
|
function importPath(fromDir: string, file: string): string {
|
|
460
71
|
const p = relative(fromDir, file).split(sep).join("/").replace(/\.[^.]+$/, "");
|
|
461
72
|
return p.startsWith(".") ? p : `./${p}`;
|
|
@@ -487,14 +98,7 @@ export async function juneBuild(
|
|
|
487
98
|
|
|
488
99
|
const contentCollections = await generateContent(appRoot);
|
|
489
100
|
// Same merge as buildManifest: app/ takes priority over .june/routes/.
|
|
490
|
-
const
|
|
491
|
-
const appRoutes2 = await scanRoutes(appDir);
|
|
492
|
-
const frameworkRoutes2 = existsSync(juneRoutesDir2) ? await scanRoutes(juneRoutesDir2) : [];
|
|
493
|
-
const appPaths2 = new Set(appRoutes2.map((r) => r.path));
|
|
494
|
-
const routes = [
|
|
495
|
-
...appRoutes2,
|
|
496
|
-
...frameworkRoutes2.filter((r) => !appPaths2.has(r.path)),
|
|
497
|
-
].sort((a, b) => a.path.localeCompare(b.path));
|
|
101
|
+
const routes = await scanAppRoutes(appRoot);
|
|
498
102
|
if (routes.length === 0) throw new Error(`no page.* routes found under ${appDir} or .june/routes/`);
|
|
499
103
|
|
|
500
104
|
const frozen = await freezeConfig(appRoot);
|
|
@@ -521,6 +125,52 @@ export async function juneBuild(
|
|
|
521
125
|
config: fullConfig,
|
|
522
126
|
});
|
|
523
127
|
|
|
128
|
+
// ---- durable agent (the F7 mount): compile app/agent → _agent.gen.ts ------
|
|
129
|
+
// The generated entry mounts the compiled module statically (fs discovery
|
|
130
|
+
// can't run on workerd) and exports the per-session Durable Object class.
|
|
131
|
+
// Only a target with Durable Objects can host the session actor — elsewhere
|
|
132
|
+
// the agent surface is skipped with a notice, and an app without an agent/
|
|
133
|
+
// directory emits a byte-identical entry (the parity guarantee).
|
|
134
|
+
const agentDir = join(appDir, frozen.agent.runtime.dir);
|
|
135
|
+
let agentModule: { file: string } | null = null;
|
|
136
|
+
if (frozen.agent.runtime.enabled) {
|
|
137
|
+
const generated = generateAgentModule(agentDir);
|
|
138
|
+
if (generated && adapter.capabilities.durableObjects) {
|
|
139
|
+
agentModule = generated;
|
|
140
|
+
} else if (generated) {
|
|
141
|
+
console.warn(
|
|
142
|
+
`[june build] app/${frozen.agent.runtime.dir}/ found, but the ${adapter.name}() target has no Durable Objects — the durable agent is not mounted.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (agentModule) {
|
|
147
|
+
// The generated DO's model is Claude, and workerd has no runtime module
|
|
148
|
+
// resolution — the SDK must be a real dependency of the app so the entry's
|
|
149
|
+
// STATIC import bundles it (@junejs/core treats it as an optional peer; the
|
|
150
|
+
// adapter's own lazy import can never be bundled). Fail the build with the
|
|
151
|
+
// fix, never ship a worker whose first model turn dies on a bare import.
|
|
152
|
+
// An explicit node_modules walk, not a resolver API: the check must behave
|
|
153
|
+
// identically on every host (Bun.resolveSync missed a symlinked scoped
|
|
154
|
+
// package on Linux CI that it found on macOS), and Rolldown does its own
|
|
155
|
+
// resolution at bundle time anyway — this is a preflight, not the resolver.
|
|
156
|
+
const sdkResolvable = (fromDir: string): boolean => {
|
|
157
|
+
// Absolute first: with a relative root (juneBuild(".")), dirname(".") is
|
|
158
|
+
// still "." and the walk would stop before reaching a hoisted parent.
|
|
159
|
+
for (let d = resolve(fromDir); ; ) {
|
|
160
|
+
if (existsSync(join(d, "node_modules", "@anthropic-ai", "sdk", "package.json"))) return true;
|
|
161
|
+
const parent = dirname(d);
|
|
162
|
+
if (parent === d) return false;
|
|
163
|
+
d = parent;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
if (!sdkResolvable(appRoot)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`app/${frozen.agent.runtime.dir}/ mounts a durable agent whose model is Claude — ` +
|
|
169
|
+
`add the SDK to the app so it bundles for workerd: bun add @anthropic-ai/sdk`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
524
174
|
// Compile the global stylesheet ONCE and content-hash it: the built worker and
|
|
525
175
|
// prerendered HTML link `/global.<hash>.css`, served immutable (cache forever,
|
|
526
176
|
// a content change ships a new URL → no revalidation, no stale window). Dev
|
|
@@ -650,6 +300,12 @@ export async function juneBuild(
|
|
|
650
300
|
const resourceRoutesField = resources.length
|
|
651
301
|
? `\n resourceRoutes: [\n${resources.join("\n")}\n ],`
|
|
652
302
|
: "";
|
|
303
|
+
// .june/routes/ patterns rank after every app/ route (dev falls back to them).
|
|
304
|
+
// Only emitted when there are some, so bundles without them stay byte-identical.
|
|
305
|
+
const generatedPaths = routes.filter((r) => r.generated).map((r) => r.path);
|
|
306
|
+
const generatedRoutesField = generatedPaths.length
|
|
307
|
+
? `\n generatedRoutes: ${JSON.stringify(generatedPaths)},`
|
|
308
|
+
: "";
|
|
653
309
|
// Only emitted when some route declares a boundary, so boundary-less bundles
|
|
654
310
|
// stay byte-identical (additive manifest field, like resources).
|
|
655
311
|
const layoutBoundariesField = boundaries.length
|
|
@@ -664,7 +320,9 @@ export async function juneBuild(
|
|
|
664
320
|
// The Link header is frozen here from the same builder the pipeline uses, so
|
|
665
321
|
// the static and dynamic surfaces advertise identically. The adapter wraps the
|
|
666
322
|
// portable pipeline for its target (workers() → withAssets).
|
|
667
|
-
|
|
323
|
+
// (catalogs: the same publication rule as the pipeline's catalogOrigin — a
|
|
324
|
+
// basePath site publishes no /.well-known catalog, so it links none.)
|
|
325
|
+
const linkHeader = buildLinkHeader(frozen.agent, { catalogs: !frozen.document.basePath });
|
|
668
326
|
const adapterEntry = adapter.entry({ linkHeader });
|
|
669
327
|
|
|
670
328
|
// Resources (when declared) are bound from the worker's env (env.DB → D1) by
|
|
@@ -691,11 +349,15 @@ export async function juneBuild(
|
|
|
691
349
|
} else if (hasResources) {
|
|
692
350
|
imports.push(`import { bindWorkerResources } from "@junejs/server/resources";`);
|
|
693
351
|
}
|
|
694
|
-
|
|
695
|
-
|
|
352
|
+
// One expression, TWO instances: the pipeline (createWorker) and the durable
|
|
353
|
+
// agent's DO each open their own provider — the DO is a separate isolate whose
|
|
354
|
+
// env must not be captured by the worker-side memoization.
|
|
355
|
+
const resourcesExpr = tursoDb
|
|
356
|
+
? `memoizeResources({ db: turso() })`
|
|
696
357
|
: hasResources
|
|
697
|
-
?
|
|
698
|
-
:
|
|
358
|
+
? `bindWorkerResources(${JSON.stringify(resourceFlags)})`
|
|
359
|
+
: null;
|
|
360
|
+
const resourcesField = resourcesExpr ? `\n resources: ${resourcesExpr},` : "";
|
|
699
361
|
|
|
700
362
|
// Opt-in Tier-3 data layer: import its installDataLayer from the declared module
|
|
701
363
|
// and call it at worker boot — the prod twin of the dev host's dataLayer.install()
|
|
@@ -723,17 +385,63 @@ export async function juneBuild(
|
|
|
723
385
|
}
|
|
724
386
|
const servicesField = servicesModule ? `\n services: __appServices,` : "";
|
|
725
387
|
|
|
388
|
+
// Durable agent: import the compiled agent module + the DO shell pieces, set
|
|
389
|
+
// agentName on the manifest (what activates createWorker's chat routing), and
|
|
390
|
+
// export the DO class the adapter's wrangler binding names. cloudflare:workers
|
|
391
|
+
// is a workerd runtime module — external to the bundle (see the predicate below).
|
|
392
|
+
if (agentModule) {
|
|
393
|
+
imports.push(`import __agentModule from ${JSON.stringify(importPath(genDir, agentModule.file))};`);
|
|
394
|
+
imports.push(`import { assembleDurable } from "@junejs/core/agent-config";`);
|
|
395
|
+
imports.push(`import { anthropic } from "@junejs/core/agent-models";`);
|
|
396
|
+
imports.push(`import { AgentDurableObject } from "@junejs/server/agent-durable";`);
|
|
397
|
+
imports.push(`import { DurableObject } from "cloudflare:workers";`);
|
|
398
|
+
// STATIC so Rolldown bundles the SDK (the adapter's own import is a
|
|
399
|
+
// non-literal lazy specifier — deliberately unbundleable); injected via
|
|
400
|
+
// anthropic({ client }), which skips that lazy path entirely.
|
|
401
|
+
imports.push(`import Anthropic from "@anthropic-ai/sdk";`);
|
|
402
|
+
}
|
|
403
|
+
// Assembled BEFORE createWorker: the manifest consumes the channels (worker-
|
|
404
|
+
// side webhook routing) and the DO class consumes the full definition.
|
|
405
|
+
const agentPreamble = agentModule ? `\nconst __agentDef = assembleDurable(__agentModule);\n` : "";
|
|
406
|
+
const agentNameField = agentModule ? `\n agentName: __agentModule.config.name,\n agentChannels: __agentDef.channels,` : "";
|
|
407
|
+
const doClass = agentModule
|
|
408
|
+
? `
|
|
409
|
+
// The durable agent: one Durable Object per session, the compiled agent/
|
|
410
|
+
// directory as its definition — adapted tools (+ read_skill when skills exist),
|
|
411
|
+
// the assembled system prompt, channel factories resolved with the DO's own
|
|
412
|
+
// env, and the app's declared resources opened from THIS isolate's env (lazily,
|
|
413
|
+
// at the first turn — the same ambient db a tool sees in native dev). Model:
|
|
414
|
+
// Anthropic over a statically bundled client (workerd cannot resolve a bare
|
|
415
|
+
// import at runtime), keyed by the ANTHROPIC_API_KEY secret — a missing secret
|
|
416
|
+
// surfaces as the SDK's own clear construction error on the first agent request.
|
|
417
|
+
export class JuneAgentDO extends DurableObject {
|
|
418
|
+
#agent = new AgentDurableObject(this.ctx, {
|
|
419
|
+
...__agentDef,
|
|
420
|
+
model: anthropic({ model: __agentModule.config.model, client: new Anthropic({ apiKey: (this.env as { ANTHROPIC_API_KEY?: string }).ANTHROPIC_API_KEY }) }),
|
|
421
|
+
env: this.env,${resourcesExpr ? `\n resources: ${resourcesExpr},` : ""}${servicesModule ? "\n services: __appServices(this.env)," : ""}
|
|
422
|
+
});
|
|
423
|
+
fetch(req: Request): Promise<Response> {
|
|
424
|
+
return this.#agent.fetch(req);
|
|
425
|
+
}
|
|
426
|
+
// Delivers input announcements left undelivered — a failed hook's retry, or the watchdog (#260).
|
|
427
|
+
alarm(): Promise<void> {
|
|
428
|
+
return this.#agent.alarm();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
`
|
|
432
|
+
: "";
|
|
433
|
+
|
|
726
434
|
const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
|
|
727
435
|
${adapterEntry.imports.join("\n")}
|
|
728
436
|
${imports.join("\n")}
|
|
729
|
-
${dataLayerBoot}
|
|
437
|
+
${dataLayerBoot}${agentPreamble}
|
|
730
438
|
const pipeline = createWorker({
|
|
731
439
|
routes: {
|
|
732
440
|
${statics.join("\n")}
|
|
733
441
|
},
|
|
734
442
|
dynamicRoutes: [
|
|
735
443
|
${dynamics.join("\n")}
|
|
736
|
-
],${resourceRoutesField}
|
|
444
|
+
],${resourceRoutesField}${generatedRoutesField}
|
|
737
445
|
layoutChains: {
|
|
738
446
|
${chains.join("\n")}
|
|
739
447
|
},${layoutBoundariesField}
|
|
@@ -741,12 +449,12 @@ ${chains.join("\n")}
|
|
|
741
449
|
${loadings.join("\n")}
|
|
742
450
|
},
|
|
743
451
|
document: ${JSON.stringify(frozen.document, null, 2).replace(/\n/g, "\n ")},
|
|
744
|
-
agent: ${JSON.stringify(frozen.agent)},${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
|
|
452
|
+
agent: ${JSON.stringify(frozen.agent)},${agentNameField}${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
|
|
745
453
|
earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}${servicesField}
|
|
746
454
|
});
|
|
747
455
|
|
|
748
456
|
${adapterEntry.wrap("pipeline")}
|
|
749
|
-
`;
|
|
457
|
+
${doClass}`;
|
|
750
458
|
const entryFile = join(genDir, "worker-entry.tsx");
|
|
751
459
|
await writeFile(entryFile, entry);
|
|
752
460
|
|
|
@@ -773,6 +481,8 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
773
481
|
external: (id: string) => {
|
|
774
482
|
// Bun built-ins exist only at Bun runtime, never in the workerd graph (see isBunSpecifier).
|
|
775
483
|
if (isBunSpecifier(id)) return true;
|
|
484
|
+
// workerd runtime modules (the durable-agent entry imports cloudflare:workers).
|
|
485
|
+
if (id.startsWith("cloudflare:")) return true;
|
|
776
486
|
// Binary assets stay external — wrangler's CompiledWasm/Data rules own them.
|
|
777
487
|
if (/\.(wasm|ttf|otf|woff2?|png|jpe?g|avif|webp)$/.test(id)) return true;
|
|
778
488
|
// Merge: adapter.buildExternal (target-specific, e.g. workers-og for the
|
|
@@ -808,7 +518,16 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
808
518
|
if (cssAsset) manifest.document.styles = `/${cssAsset}`; // prerendered HTML links the hashed sheet
|
|
809
519
|
if (clientAsset) manifest.document.clientScript = `/${clientAsset}`;
|
|
810
520
|
if (moduleCssAsset) manifest.document.moduleStyles = `/${moduleCssAsset}`;
|
|
811
|
-
|
|
521
|
+
// A static site has no server: /mcp (and the WebMCP tools that proxy to it)
|
|
522
|
+
// and the HTTP API (POST /api/<id>, /openapi.json) do not exist there. Render
|
|
523
|
+
// with both projected out, so the published files advertise only what is
|
|
524
|
+
// deployed: llms.txt drops its MCP and HTTP API sections, the catalogs and the
|
|
525
|
+
// generated skill list neither, pages register no WebMCP tools.
|
|
526
|
+
const isStatic = adapter.capabilities.runtime === "static";
|
|
527
|
+
if (isStatic) manifest.agent = { ...manifest.agent, mcp: false, api: false, webmcp: false };
|
|
528
|
+
// The static() target prerenders its sitemap too; that build is where the
|
|
529
|
+
// route contract lets `staticPaths` run, so its sitemap lists those pages.
|
|
530
|
+
const worker = createWorker(manifest, { staticBuild: isStatic });
|
|
812
531
|
let hasAssets = false;
|
|
813
532
|
|
|
814
533
|
// ---- public/ → assets/ : verbatim static files (favicon, images, fonts) ----
|
|
@@ -846,11 +565,29 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
846
565
|
if (publicFiles.length) hasAssets = true;
|
|
847
566
|
}
|
|
848
567
|
|
|
568
|
+
// ---- default PNG icons: apple-touch-icon.png, icon.png, favicon.ico -----
|
|
569
|
+
// Only when the app brings no icon of its own (the frozen document already
|
|
570
|
+
// links them — same resolveIcons call). Listed with publicFiles so adapters
|
|
571
|
+
// with a separate static tier (vercel) place them like any public/ file.
|
|
572
|
+
const site = frozen.document.site;
|
|
573
|
+
if (resolveIcons(site, publicFileCheck(publicDir)).generate) {
|
|
574
|
+
const icons = await renderIcons({
|
|
575
|
+
site,
|
|
576
|
+
fetchFont: googleFontFetcher(join(appRoot, "node_modules", ".cache", "june", "fonts")),
|
|
577
|
+
warn: (msg) => console.warn(msg),
|
|
578
|
+
});
|
|
579
|
+
await mkdir(assetsDir, { recursive: true }); // an app with no public/ has none yet
|
|
580
|
+
for (const file of GENERATED_ICONS) {
|
|
581
|
+
await writeFile(join(assetsDir, file), icons[file]);
|
|
582
|
+
publicFiles.push(file);
|
|
583
|
+
}
|
|
584
|
+
hasAssets = true;
|
|
585
|
+
}
|
|
586
|
+
|
|
849
587
|
// static() target: prerender EVERY route (not just opted-in ones) + enumerate
|
|
850
588
|
// dynamic routes via their staticPaths, and write <stem>/index.html so clean URLs
|
|
851
589
|
// resolve on a dumb file host with no rewrite server. Other targets keep the
|
|
852
590
|
// opt-in `prerender: true` behavior and the flat <stem>.html naming (byte-identical).
|
|
853
|
-
const isStatic = adapter.capabilities.runtime === "static";
|
|
854
591
|
const i18n = frozen.i18n;
|
|
855
592
|
|
|
856
593
|
// Render ONE route pathname through the worker → HTML (+ .md/.json projections).
|
|
@@ -875,7 +612,7 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
875
612
|
if (def.md !== false) targets.push([mdReq, mdFile]); // .md/.json stay flat (exact-path negotiation)
|
|
876
613
|
if (typeof def.json === "function") targets.push([jsonReq, jsonFile]);
|
|
877
614
|
for (const [rp, file] of targets) {
|
|
878
|
-
const res = await worker.fetch(new Request(
|
|
615
|
+
const res = await worker.fetch(new Request(`${PRERENDER_ORIGIN}${rp}`));
|
|
879
616
|
if (!res.ok) throw new Error(`prerender ${rp} → ${res.status}`);
|
|
880
617
|
const dest = join(assetsDir, file);
|
|
881
618
|
await mkdir(dirname(dest), { recursive: true });
|
|
@@ -887,6 +624,17 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
887
624
|
|
|
888
625
|
// Locale variants a static route is emitted at: no i18n → [path]; with i18n → one
|
|
889
626
|
// per locale (defaultLocale keeps the bare path, others get their localeHref prefix).
|
|
627
|
+
// A static build is ONE file tree on ONE host: a locale on its own domain has no
|
|
628
|
+
// path in it (and a basePath can't apply there), so say so instead of failing
|
|
629
|
+
// later with a prerender 404.
|
|
630
|
+
if (isStatic && i18n) {
|
|
631
|
+
const onDomain = Object.keys(i18n.locales).filter((l) => i18n.locales[l]!.domain);
|
|
632
|
+
if (onDomain.length) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`[june build] the static() target serves one host, so i18n locales with their own domain are not supported (${onDomain.join(", ")}). Give them a path prefix instead, e.g. { path: "/${onDomain[0]}" }.`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
890
638
|
const localeVariants = (path: string): string[] =>
|
|
891
639
|
isStatic && i18n
|
|
892
640
|
? [...new Set(Object.keys(i18n.locales).map((l) => localeHref(i18n, path, l)))]
|
|
@@ -907,6 +655,9 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
907
655
|
const sp = dyn.def.staticPaths;
|
|
908
656
|
if (!sp) continue;
|
|
909
657
|
const paths = typeof sp === "function" ? await sp() : sp;
|
|
658
|
+
// Resolve a producer ONCE: the prerendered sitemap below reads this same
|
|
659
|
+
// def, so it lists exactly the set prerendered here without a second call.
|
|
660
|
+
dyn.def.staticPaths = paths;
|
|
910
661
|
for (const p of paths) await prerenderOne(p, dyn.def);
|
|
911
662
|
}
|
|
912
663
|
// Framework surfaces the worker would otherwise generate on the fly. Guarded on
|
|
@@ -914,18 +665,53 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
914
665
|
// skips its file rather than failing the build.
|
|
915
666
|
const extra: Array<[string, string]> = [
|
|
916
667
|
["/favicon.svg", "favicon.svg"],
|
|
668
|
+
["/manifest.webmanifest", "manifest.webmanifest"],
|
|
917
669
|
["/llms.txt", "llms.txt"],
|
|
918
670
|
["/sitemap.xml", "sitemap.xml"],
|
|
919
671
|
];
|
|
920
|
-
|
|
921
|
-
|
|
672
|
+
// A sitemap needs absolute URLs, and the prerender host is a placeholder: with
|
|
673
|
+
// no public origin configured the pipeline answers 404 for it (and the loop
|
|
674
|
+
// below skips it), so say why rather than silently ship no sitemap.
|
|
675
|
+
if (manifest.agent.discovery && !manifest.document.site.url && !manifest.document.deployOrigin) {
|
|
676
|
+
console.warn(
|
|
677
|
+
"[june build] no sitemap.xml: set site.url (or deploy.domain) in june.config.ts so its URLs are absolute.",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
// The agent catalogs (ARD, Agent Skills) — JSON/Markdown files a static host
|
|
681
|
+
// can serve as-is — only when this site publishes them: agent discovery on, a
|
|
682
|
+
// root deploy (a basePath site doesn't own /.well-known), and a known public
|
|
683
|
+
// origin (the files name absolute URLs). Same rule as the pipeline's
|
|
684
|
+
// catalogOrigin, stated here too so an app catch-all route can never answer
|
|
685
|
+
// these paths into the build. The generated skill's path carries its name, so
|
|
686
|
+
// it is read back from the index.
|
|
687
|
+
const doc = manifest.document;
|
|
688
|
+
if (manifest.agent.discovery && !doc.basePath && (doc.site.url || doc.deployOrigin)) {
|
|
689
|
+
extra.push(
|
|
690
|
+
["/.well-known/ai-catalog.json", ".well-known/ai-catalog.json"],
|
|
691
|
+
["/.well-known/ard.json", ".well-known/ard.json"],
|
|
692
|
+
["/.well-known/agent-skills/index.json", ".well-known/agent-skills/index.json"],
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
for (let i = 0; i < extra.length; i++) {
|
|
696
|
+
const [reqPath, file] = extra[i]!;
|
|
697
|
+
const res = await worker.fetch(new Request(`${PRERENDER_ORIGIN}${reqPath}`));
|
|
922
698
|
if (!res.ok) continue;
|
|
923
|
-
|
|
699
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
700
|
+
await mkdir(dirname(join(assetsDir, file)), { recursive: true });
|
|
701
|
+
await writeFile(join(assetsDir, file), bytes);
|
|
924
702
|
hasAssets = true;
|
|
703
|
+
if (file.endsWith("agent-skills/index.json")) {
|
|
704
|
+
const index = JSON.parse(bytes.toString("utf8")) as { skills?: Array<{ url?: string }> };
|
|
705
|
+
for (const s of index.skills ?? []) {
|
|
706
|
+
if (!s.url) continue;
|
|
707
|
+
const p = new URL(s.url, PRERENDER_ORIGIN).pathname;
|
|
708
|
+
extra.push([p, p.slice(1)]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
925
711
|
}
|
|
926
712
|
// 404.html — GitHub Pages serves it for any unmatched URL. A deliberately-missing
|
|
927
713
|
// path renders June's not-found HTML (body is written regardless of the 404 status).
|
|
928
|
-
const nf = await worker.fetch(new Request(
|
|
714
|
+
const nf = await worker.fetch(new Request(`${PRERENDER_ORIGIN}/__june_not_found__`));
|
|
929
715
|
await writeFile(join(assetsDir, "404.html"), Buffer.from(await nf.arrayBuffer()));
|
|
930
716
|
hasAssets = true;
|
|
931
717
|
}
|
|
@@ -960,16 +746,23 @@ ${adapterEntry.wrap("pipeline")}
|
|
|
960
746
|
const defaultName = workerName(pkgName ?? basename(appRoot));
|
|
961
747
|
// A declared D1-backed db (sqlite/d1) → a D1 binding named DB (the name
|
|
962
748
|
// bindWorkerResources reads). turso() connects from env, not a binding — no plan.
|
|
963
|
-
const plan: ResourcePlan =
|
|
964
|
-
resourcesCfg?.db && resourcesCfg.db.kind !== "turso"
|
|
749
|
+
const plan: ResourcePlan = {
|
|
750
|
+
...(resourcesCfg?.db && resourcesCfg.db.kind !== "turso"
|
|
965
751
|
? { db: { binding: "DB", databaseName: `${defaultName}-db` } }
|
|
966
|
-
: {}
|
|
752
|
+
: {}),
|
|
753
|
+
// The durable agent's DO binding — the name createWorker reads (env.AGENT)
|
|
754
|
+
// and the class the generated entry exports.
|
|
755
|
+
...(agentModule ? { agent: { binding: "AGENT", className: "JuneAgentDO" } } : {}),
|
|
756
|
+
};
|
|
967
757
|
await adapter.emit({ appRoot, outDir, hasAssets, linkHeader, config: fullConfig, plan, defaultName, publicFiles });
|
|
968
758
|
if (plan.db) {
|
|
969
759
|
console.log(
|
|
970
760
|
` ↳ d1 binding "${plan.db.binding}" emitted — set database_id in wrangler.jsonc (wrangler d1 create ${plan.db.databaseName})`,
|
|
971
761
|
);
|
|
972
762
|
}
|
|
763
|
+
if (plan.agent) {
|
|
764
|
+
console.log(` ↳ durable agent mounted (${plan.agent.className} → env.${plan.agent.binding}; chat at ${frozen.agent.runtime.chat.path})`);
|
|
765
|
+
}
|
|
973
766
|
|
|
974
767
|
return {
|
|
975
768
|
outFile,
|