@junejs/server 0.1.3 → 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 -30
- package/dist/build.js +135 -296
- 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 +6 -7
- 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 +51 -38
- 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 -57
- package/dist/worker.js +69 -27
- 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 +230 -446
- 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-scan.ts +161 -0
- package/src/router.ts +66 -54
- 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 +131 -35
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Filesystem route discovery for the build: walk app/ for page.* files and
|
|
2
|
+
// merge in the framework's .june/routes/ slot. Extracted from build.ts so the
|
|
3
|
+
// scan/merge rules live beside each other, in one place — buildManifest and
|
|
4
|
+
// juneBuild consume the SAME merged list via scanAppRoutes (they previously
|
|
5
|
+
// each inlined the merge).
|
|
6
|
+
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { readdir } from "node:fs/promises";
|
|
9
|
+
import { basename, join, relative, sep } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { parseSegment } from "./route-rank";
|
|
12
|
+
|
|
13
|
+
// The segment layout CHAIN root→leaf: every directory level (route groups
|
|
14
|
+
// included) may contribute a layout.* that wraps routes below it.
|
|
15
|
+
export type RouteEntry = {
|
|
16
|
+
path: string;
|
|
17
|
+
file: string;
|
|
18
|
+
dynamic: boolean;
|
|
19
|
+
resource?: boolean; // a route.* resource route (raw-Response handler), not a page
|
|
20
|
+
generated?: boolean; // scanned from .june/routes/ — ranks after every app/ route, as in dev
|
|
21
|
+
layouts: string[];
|
|
22
|
+
loading?: string; // nearest loading.tsx up the tree → streaming Suspense fallback
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const PAGE_BASENAMES = new Set(["page", "index"]);
|
|
26
|
+
const ROUTE_EXTS = [".tsx", ".jsx", ".ts", ".js"];
|
|
27
|
+
|
|
28
|
+
const isRouteGroup = (name: string) => /^\(.+\)$/.test(name);
|
|
29
|
+
|
|
30
|
+
function segmentFile(dir: string, base: string): string | undefined {
|
|
31
|
+
return ROUTE_EXTS.map((e) => join(dir, `${base}${e}`)).find(existsSync);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Walk app/ for page.* files → route paths (mirrors router.ts conventions:
|
|
35
|
+
// route groups vanish from URLs, `_`-prefixed entries are private), carrying the
|
|
36
|
+
// layout chain accumulated from each directory level.
|
|
37
|
+
export async function scanRoutes(
|
|
38
|
+
appDir: string,
|
|
39
|
+
dir = appDir,
|
|
40
|
+
layouts: string[] = [],
|
|
41
|
+
out: RouteEntry[] = [],
|
|
42
|
+
loading?: string,
|
|
43
|
+
): Promise<RouteEntry[]> {
|
|
44
|
+
const ownLayout = segmentFile(dir, "layout");
|
|
45
|
+
const chain = ownLayout ? [...layouts, ownLayout] : layouts;
|
|
46
|
+
const nearestLoading = segmentFile(dir, "loading") ?? loading;
|
|
47
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
if (e.name.startsWith("_") || e.name.startsWith(".")) continue;
|
|
50
|
+
const full = join(dir, e.name);
|
|
51
|
+
if (e.isDirectory()) {
|
|
52
|
+
await scanRoutes(appDir, full, chain, out, nearestLoading);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const ext = e.name.match(/\.[^.]+$/)?.[0] ?? "";
|
|
56
|
+
if (!ROUTE_EXTS.includes(ext)) continue;
|
|
57
|
+
const base = basename(e.name, ext);
|
|
58
|
+
const resource = base === "route";
|
|
59
|
+
if (!PAGE_BASENAMES.has(base) && !resource) continue;
|
|
60
|
+
const relDir = relative(appDir, dir);
|
|
61
|
+
const segments = relDir === "" ? [] : relDir.split(sep).filter((s) => !isRouteGroup(s));
|
|
62
|
+
const path = "/" + segments.join("/");
|
|
63
|
+
out.push({
|
|
64
|
+
path: path === "/" ? "/" : path,
|
|
65
|
+
file: full,
|
|
66
|
+
dynamic: /\[.+\]/.test(path),
|
|
67
|
+
resource,
|
|
68
|
+
layouts: chain,
|
|
69
|
+
loading: nearestLoading,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Merge routes: app/ takes priority over .june/routes/ (app/ is the escape hatch).
|
|
76
|
+
// .june/routes/ is the convention slot for framework-generated routes (e.g. kura
|
|
77
|
+
// writes its docs/search/og routes there so the user never manages boilerplate).
|
|
78
|
+
export async function scanAppRoutes(appRoot: string): Promise<RouteEntry[]> {
|
|
79
|
+
const appDir = join(appRoot, "app");
|
|
80
|
+
const juneRoutesDir = join(appRoot, ".june", "routes");
|
|
81
|
+
const appRoutes = await scanRoutes(appDir);
|
|
82
|
+
const frameworkRoutes = existsSync(juneRoutesDir) ? await scanRoutes(juneRoutesDir) : [];
|
|
83
|
+
const problems = routeProblems([appRoutes, frameworkRoutes], appRoot);
|
|
84
|
+
if (problems) throw new Error(problems);
|
|
85
|
+
const appPaths = new Set(appRoutes.map((r) => r.path));
|
|
86
|
+
const generated = frameworkRoutes
|
|
87
|
+
.filter((r) => !appPaths.has(r.path))
|
|
88
|
+
.map((r) => ({ ...r, generated: true }));
|
|
89
|
+
return [...appRoutes, ...generated].sort((a, b) =>
|
|
90
|
+
a.path.localeCompare(b.path),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Two route files in ONE tree that resolve to the same URL: `(a)/about/page.tsx`
|
|
95
|
+
// and `(b)/about/page.tsx` (groups vanish from the URL), or `page.tsx` next to
|
|
96
|
+
// `index.tsx`. Pages and resource routes are counted apart — a page and a
|
|
97
|
+
// route.ts at the same path are allowed, in one dir or across groups (the page
|
|
98
|
+
// wins). No winner is defined for the rest: dev
|
|
99
|
+
// would pick by group order and the worker by scan order, so the build refuses
|
|
100
|
+
// them and dev reports them (#316).
|
|
101
|
+
export function routeConflicts(routes: RouteEntry[]): Array<{ path: string; files: string[] }> {
|
|
102
|
+
const byKey = new Map<string, { path: string; files: string[] }>();
|
|
103
|
+
for (const r of routes) {
|
|
104
|
+
const key = `${r.resource ? "route" : "page"} ${r.path}`;
|
|
105
|
+
const hit = byKey.get(key);
|
|
106
|
+
if (hit) hit.files.push(r.file);
|
|
107
|
+
else byKey.set(key, { path: r.path, files: [r.file] });
|
|
108
|
+
}
|
|
109
|
+
return [...byKey.values()]
|
|
110
|
+
.filter((c) => c.files.length > 1)
|
|
111
|
+
.map((c) => ({ path: c.path, files: c.files.sort() }))
|
|
112
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// An optional or catch-all segment ([[x]], [...x], [[...x]]) must END the route
|
|
116
|
+
// path (#315, the Next.js rule, opinionated). Mid-path there is no behavior to
|
|
117
|
+
// keep: dev never matched past a catch-all nor skipped a mid-path [[x]], while
|
|
118
|
+
// the worker's regex did both. A (group) after it is not a URL segment, so it's
|
|
119
|
+
// fine. Returns the offending routes, path-sorted.
|
|
120
|
+
const ENDS_PATH = new Set(["optional", "catchAll", "optionalCatchAll"]);
|
|
121
|
+
const beforeLast = (path: string) => path.split("/").filter(Boolean).slice(0, -1).map((s) => parseSegment(s).kind);
|
|
122
|
+
|
|
123
|
+
export function misplacedSegments(routes: RouteEntry[]): RouteEntry[] {
|
|
124
|
+
return routes
|
|
125
|
+
.filter((r) => beforeLast(r.path).some((k) => ENDS_PATH.has(k)))
|
|
126
|
+
.sort((a, b) => a.path.localeCompare(b.path) || a.file.localeCompare(b.file));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Every route-table problem the build refuses and dev reports, as one message
|
|
130
|
+
// (null when there are none). Each tree (app/, .june/routes/) is checked on its
|
|
131
|
+
// own: the same path in both is not a conflict — app/ wins.
|
|
132
|
+
export function routeProblems(trees: RouteEntry[][], appRoot: string): string | null {
|
|
133
|
+
const rel = (f: string) => relative(appRoot, f).split(sep).join("/");
|
|
134
|
+
const sections: string[] = [];
|
|
135
|
+
|
|
136
|
+
const conflicts = trees.flatMap(routeConflicts);
|
|
137
|
+
if (conflicts.length) {
|
|
138
|
+
sections.push(
|
|
139
|
+
"[june] more than one route file resolves to the same path — keep one per path:\n" +
|
|
140
|
+
conflicts.map((c) => ` ${c.path}: ${c.files.map(rel).join(", ")}`).join("\n"),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const misplaced = trees.flatMap(misplacedSegments);
|
|
145
|
+
if (misplaced.length) {
|
|
146
|
+
const lines = misplaced.map((r) => ` ${r.path}: ${rel(r.file)}`);
|
|
147
|
+
// Developers (and models) read [[x]] the SvelteKit way, skippable anywhere;
|
|
148
|
+
// say so when a single optional is the culprit, and point at the real tool.
|
|
149
|
+
if (misplaced.some((r) => beforeLast(r.path).includes("optional"))) {
|
|
150
|
+
lines.push(
|
|
151
|
+
" Unlike SvelteKit, June does not skip a [[param]] mid-path. For a locale prefix, set i18n.locales in june.config.ts instead of a [[lang]] directory.",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
sections.push(
|
|
155
|
+
"[june] an optional or catch-all segment must be the last segment of a route path — nothing may follow it:\n" +
|
|
156
|
+
lines.join("\n"),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return sections.length ? sections.join("\n") : null;
|
|
161
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -151,8 +151,10 @@ function routePath(appDir: string, file: string) {
|
|
|
151
151
|
// Recursive-descent matcher over the app directory. Priority at every level:
|
|
152
152
|
// exact static segment > [param] > [...catchAll], required before optional, ties
|
|
153
153
|
// by name (route-rank.ts — the built worker sorts its route table by the same
|
|
154
|
-
// ranking);
|
|
155
|
-
//
|
|
154
|
+
// ranking); `_`-prefixed entries never participate. Route groups `(name)` are
|
|
155
|
+
// invisible in the URL, so they are invisible to ranking too: a group's children
|
|
156
|
+
// compete as siblings of the level the group sits in (a grouped `[slug]` never
|
|
157
|
+
// shadows a static sibling), while the group still contributes its layout.
|
|
156
158
|
// Returns the page file, accumulated params (catch-all joins with "/"), and the
|
|
157
159
|
// chain of segments (with their special files) from the app root to the page.
|
|
158
160
|
export async function matchRouteTree(
|
|
@@ -162,86 +164,96 @@ export async function matchRouteTree(
|
|
|
162
164
|
): Promise<RouteTreeMatch | null> {
|
|
163
165
|
const urlSegments = pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
164
166
|
|
|
167
|
+
// One URL level = this dir plus every route group under it (groups nest), each
|
|
168
|
+
// with the segment chain down to it: the dir first, then groups in name order.
|
|
169
|
+
type Level = { dir: string; entries: DirEntry[]; segments: SegmentMatch[] };
|
|
170
|
+
async function levelAt(dir: string, chain: SegmentMatch[]): Promise<Level[]> {
|
|
171
|
+
// Ranked, not readdir order, so the pick is the same on every filesystem.
|
|
172
|
+
const entries = (await listDir(dir)).sort((a, b) => compareSegments(a.name, b.name));
|
|
173
|
+
const segments = [...chain, segmentAt(dir, entries)];
|
|
174
|
+
const levels: Level[] = [{ dir, entries, segments }];
|
|
175
|
+
for (const e of entries) {
|
|
176
|
+
if (e.dir && isRouteGroup(e.name)) levels.push(...(await levelAt(join(dir, e.name), segments)));
|
|
177
|
+
}
|
|
178
|
+
return levels;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A URL level can span several real dirs: `(a)/blog` and `(b)/blog` are both
|
|
182
|
+
// `/blog`, so they descend TOGETHER — otherwise (a)/blog/[slug] would answer
|
|
183
|
+
// /blog/about before (b)/blog/about was ever seen.
|
|
184
|
+
type Node = { dir: string; chain: SegmentMatch[] };
|
|
185
|
+
|
|
165
186
|
async function descend(
|
|
166
|
-
|
|
187
|
+
nodes: Node[],
|
|
167
188
|
rest: string[],
|
|
168
189
|
params: Record<string, string>,
|
|
169
|
-
chain: SegmentMatch[],
|
|
170
190
|
): Promise<RouteTreeMatch | null> {
|
|
171
|
-
|
|
172
|
-
const entries = (await listDir(dir)).sort((a, b) => compareSegments(a.name, b.name));
|
|
173
|
-
const segments = [...chain, segmentAt(dir, entries)];
|
|
191
|
+
const levels = (await Promise.all(nodes.map((n) => levelAt(n.dir, n.chain)))).flat();
|
|
174
192
|
|
|
175
|
-
// Terminal: URL consumed → find the page
|
|
193
|
+
// Terminal: URL consumed → find the page at this level, groups included,
|
|
194
|
+
// and only then a resource route: a page beats a route.ts at the same path
|
|
195
|
+
// even when the two sit in different groups (the worker ranks them so).
|
|
176
196
|
if (rest.length === 0) {
|
|
177
|
-
const page
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
197
|
+
for (const base of [["page", "index"], ["route"]]) {
|
|
198
|
+
for (const { dir: d, entries, segments } of levels) {
|
|
199
|
+
const file = base.map((b) => fileFor(entries, d, b)).find(Boolean);
|
|
200
|
+
if (file) return { file, params, segments };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else if (!options.pageConvention && rest.length === 1) {
|
|
183
204
|
// Legacy flat convention: a non-special leaf FILE names the final segment
|
|
184
205
|
// (examples/rsc: about.tsx → /about). Only valid for the last segment.
|
|
185
|
-
|
|
186
|
-
const leaf = fileFor(entries,
|
|
187
|
-
if (leaf && !isSpecialFile(leaf) && !isPageFile(leaf)) {
|
|
188
|
-
return { file: leaf, params, segments };
|
|
189
|
-
}
|
|
206
|
+
for (const { dir: d, entries, segments } of levels) {
|
|
207
|
+
const leaf = fileFor(entries, d, rest[0]!);
|
|
208
|
+
if (leaf && !isSpecialFile(leaf) && !isPageFile(leaf)) return { file: leaf, params, segments };
|
|
190
209
|
}
|
|
191
210
|
}
|
|
192
211
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
// Every child dir at this level, groups flattened and same names merged, in
|
|
213
|
+
// rank order.
|
|
214
|
+
const children = new Map<string, Node[]>();
|
|
215
|
+
for (const lv of levels) {
|
|
216
|
+
for (const e of lv.entries) {
|
|
217
|
+
if (!e.dir || isRouteGroup(e.name)) continue;
|
|
218
|
+
const node = { dir: join(lv.dir, e.name), chain: lv.segments };
|
|
219
|
+
const same = children.get(e.name);
|
|
220
|
+
if (same) same.push(node);
|
|
221
|
+
else children.set(e.name, [node]);
|
|
222
|
+
}
|
|
198
223
|
}
|
|
224
|
+
const names = [...children.keys()].sort(compareSegments);
|
|
199
225
|
|
|
200
226
|
if (rest.length === 0) {
|
|
201
227
|
// Optional segments match ABSENCE too: descend without consuming and
|
|
202
228
|
// without setting the param.
|
|
203
|
-
for (const
|
|
204
|
-
if (!
|
|
205
|
-
const hit = await descend(
|
|
229
|
+
for (const name of names) {
|
|
230
|
+
if (!(isOptionalDir(name) || isOptionalCatchAllDir(name))) continue;
|
|
231
|
+
const hit = await descend(children.get(name)!, [], params);
|
|
206
232
|
if (hit) return hit;
|
|
207
233
|
}
|
|
208
234
|
return null;
|
|
209
235
|
}
|
|
210
236
|
const [head, ...tail] = rest as [string, ...string[]];
|
|
211
237
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{ ...params, [paramName(e.name)]: head },
|
|
225
|
-
segments,
|
|
226
|
-
);
|
|
227
|
-
if (hit) return hit;
|
|
228
|
-
}
|
|
229
|
-
// 3) [...catchAll] and [[...catchAll]] dirs consume everything remaining
|
|
230
|
-
for (const e of entries) {
|
|
231
|
-
if (!e.dir || !(isCatchAllDir(e.name) || isOptionalCatchAllDir(e.name))) continue;
|
|
232
|
-
const hit = await descend(
|
|
233
|
-
join(dir, e.name),
|
|
234
|
-
[],
|
|
235
|
-
{ ...params, [paramName(e.name)]: rest.join("/") },
|
|
236
|
-
segments,
|
|
237
|
-
);
|
|
238
|
+
for (const name of names) {
|
|
239
|
+
const into = children.get(name)!;
|
|
240
|
+
let hit: RouteTreeMatch | null = null;
|
|
241
|
+
if (isParamDir(name) || isOptionalDir(name)) {
|
|
242
|
+
// [param] and [[param]] consume one segment
|
|
243
|
+
hit = await descend(into, tail, { ...params, [paramName(name)]: head });
|
|
244
|
+
} else if (isCatchAllDir(name) || isOptionalCatchAllDir(name)) {
|
|
245
|
+
// [...catchAll] and [[...catchAll]] consume everything remaining
|
|
246
|
+
hit = await descend(into, [], { ...params, [paramName(name)]: rest.join("/") });
|
|
247
|
+
} else if (name === head) {
|
|
248
|
+
hit = await descend(into, tail, params); // exact static dir
|
|
249
|
+
}
|
|
238
250
|
if (hit) return hit;
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
return null;
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
return descend(appDir, urlSegments, {}
|
|
256
|
+
return descend([{ dir: appDir, chain: [] }], urlSegments, {});
|
|
245
257
|
}
|
|
246
258
|
|
|
247
259
|
// 404 path: walk the longest matchable STATIC prefix of the URL collecting
|
|
@@ -36,7 +36,8 @@ async function streamToString(stream: ReadableStream<Uint8Array>): Promise<strin
|
|
|
36
36
|
return out;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async function renderDocument(
|
|
39
|
+
async function renderDocument(url: URL): Promise<string | null> {
|
|
40
|
+
const path = url.pathname;
|
|
40
41
|
// Non-literal specifier → rolldown leaves it as a runtime import, so the
|
|
41
42
|
// react-server server bundle is loaded (not re-bundled) from beside this file.
|
|
42
43
|
const serverUrl = new URL("./server.js", import.meta.url).href;
|
|
@@ -50,7 +51,8 @@ async function renderDocument(path: string): Promise<string | null> {
|
|
|
50
51
|
}) as unknown as Promise<React.ReactNode>;
|
|
51
52
|
|
|
52
53
|
const html = await renderToReadableStream(
|
|
53
|
-
|
|
54
|
+
// RSC routes are exact paths (no locale prefix), so the home route IS "/".
|
|
55
|
+
<Document config={DOC_CONFIG} pageUrl={url.href} isHome={path === "/"}>
|
|
54
56
|
<Root tree={tree} />
|
|
55
57
|
</Document>,
|
|
56
58
|
);
|
|
@@ -62,8 +64,7 @@ async function renderDocument(path: string): Promise<string | null> {
|
|
|
62
64
|
|
|
63
65
|
export default {
|
|
64
66
|
async fetch(request: Request): Promise<Response> {
|
|
65
|
-
const
|
|
66
|
-
const body = await renderDocument(path);
|
|
67
|
+
const body = await renderDocument(new URL(request.url));
|
|
67
68
|
if (body === null) return new Response("Not Found", { status: 404 });
|
|
68
69
|
return new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
69
70
|
},
|
package/src/sqlite-driver.ts
CHANGED
|
@@ -135,6 +135,33 @@ export function makeWarningFilter(original: EmitWarning): EmitWarning {
|
|
|
135
135
|
// runtime, which is exactly what the typeof guard checks.
|
|
136
136
|
declare const Bun: unknown;
|
|
137
137
|
|
|
138
|
+
// A file database commits durably: WAL, fsynced on every commit. SQLite's default
|
|
139
|
+
// rollback journal (DELETE mode) commits by unlinking the journal, and at
|
|
140
|
+
// synchronous=FULL it does not fsync the directory after that unlink — so a power
|
|
141
|
+
// cut right after a commit can bring the journal back, and the next open rolls the
|
|
142
|
+
// committed transaction back. On a VM power cut that lost the last committed agent
|
|
143
|
+
// turn 5 times out of 5; with WAL + FULL, 0 of 5. FULL is set explicitly: it is a
|
|
144
|
+
// build-time default, and WAL at synchronous=NORMAL skips the per-commit fsync.
|
|
145
|
+
// journal_mode=WAL persists in the file; `:memory:` has no journal to change.
|
|
146
|
+
// A PRAGMA that throws (a locked file mid-switch) closes the handle it was handed,
|
|
147
|
+
// so a retry is not blocked by a descriptor nobody can reach; the original error
|
|
148
|
+
// is what surfaces. Exported for the unit test of that path.
|
|
149
|
+
export function useDurableJournal(db: SyncSqlite, path: string): SyncSqlite {
|
|
150
|
+
if (path === ":memory:" || path === "") return db;
|
|
151
|
+
try {
|
|
152
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
153
|
+
db.exec("PRAGMA synchronous=FULL");
|
|
154
|
+
} catch (err) {
|
|
155
|
+
try {
|
|
156
|
+
db.close();
|
|
157
|
+
} catch {
|
|
158
|
+
// the setup error is the one worth reporting
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
return db;
|
|
163
|
+
}
|
|
164
|
+
|
|
138
165
|
// Open a LOCAL *synchronous* sqlite handle on whichever runtime we're under —
|
|
139
166
|
// bun:sqlite under Bun, node:sqlite under Node. This is the raw synchronous
|
|
140
167
|
// surface: asyncSqlite() wraps it as the edge-safe async JuneDb (below), while
|
|
@@ -148,7 +175,7 @@ export async function openLocalSqliteSync(path: string): Promise<SyncSqlite> {
|
|
|
148
175
|
const { Database } = (await import(specifier)) as {
|
|
149
176
|
Database: new (p: string, o?: { create?: boolean }) => SyncSqlite;
|
|
150
177
|
};
|
|
151
|
-
return new Database(path, { create: true });
|
|
178
|
+
return useDurableJournal(new Database(path, { create: true }), path);
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
// Node: node:sqlite is a builtin, but only flag-free on a recent-enough
|
|
@@ -172,11 +199,14 @@ export async function openLocalSqliteSync(path: string): Promise<SyncSqlite> {
|
|
|
172
199
|
}
|
|
173
200
|
const db = new mod.DatabaseSync(path);
|
|
174
201
|
// Adapt node:sqlite (prepare()) to the query()-shaped SyncSqlite surface.
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
return useDurableJournal(
|
|
203
|
+
{
|
|
204
|
+
query: (sql) => db.prepare(sql),
|
|
205
|
+
exec: (sql) => db.exec(sql),
|
|
206
|
+
close: () => db.close(),
|
|
207
|
+
},
|
|
208
|
+
path,
|
|
209
|
+
);
|
|
180
210
|
}
|
|
181
211
|
|
|
182
212
|
// Open a LOCAL sqlite file (or ":memory:") as the async JuneDb — the single seam
|
package/src/tsconfig-jsx.ts
CHANGED
|
@@ -9,8 +9,8 @@ import { join } from "node:path";
|
|
|
9
9
|
// tsconfig.json is JSONC by convention (comments + trailing commas are idiomatic), and a strict
|
|
10
10
|
// JSON.parse failure here silently degrades to "not declared" → the conflict warning reappears.
|
|
11
11
|
// Strip comments (`//` only at line-start/after-whitespace so "https://…" in strings survives)
|
|
12
|
-
// and trailing commas before parsing.
|
|
13
|
-
const stripJsonc = (s: string): string =>
|
|
12
|
+
// and trailing commas before parsing. Shared: agent-compile's tsconfig sniff parses the same way.
|
|
13
|
+
export const stripJsonc = (s: string): string =>
|
|
14
14
|
s
|
|
15
15
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
16
16
|
.replace(/(^|\s)\/\/.*$/gm, "$1")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// turn-events.ts — one turn's live TurnEvents as an AsyncIterable, for a consumer in the
|
|
2
|
+
// SAME process as the session: the Durable Object's delivered renders (agent-durable.ts)
|
|
3
|
+
// and the native host's ChannelContext.runStream/resumeStream (agent-native.ts).
|
|
4
|
+
|
|
5
|
+
import type { AgentSession, TurnEvent } from "@junejs/core/agent-runtime";
|
|
6
|
+
|
|
7
|
+
// Subscribes eagerly at call time — not at first iteration — so the sseTurnStream timing
|
|
8
|
+
// guarantee carries over (call this synchronously after start()/resume(), before any event
|
|
9
|
+
// can emit); events landing before the consumer catches up are buffered. Ends after the
|
|
10
|
+
// turn's terminal event (completed, failed, cancelled, or input.requested — a park ends
|
|
11
|
+
// this stream; a later resume is a new one), and an early consumer exit (for-await
|
|
12
|
+
// break/return) unsubscribes rather than buffering forever.
|
|
13
|
+
export function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable<TurnEvent> {
|
|
14
|
+
const queue: TurnEvent[] = [];
|
|
15
|
+
let terminal = false;
|
|
16
|
+
let notify: (() => void) | undefined;
|
|
17
|
+
const unsub = session.observe((e) => {
|
|
18
|
+
queue.push(e);
|
|
19
|
+
if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "turn.cancelled" || e.type === "input.requested") terminal = true;
|
|
20
|
+
notify?.();
|
|
21
|
+
}, { turnId });
|
|
22
|
+
return {
|
|
23
|
+
[Symbol.asyncIterator]() {
|
|
24
|
+
return {
|
|
25
|
+
async next(): Promise<IteratorResult<TurnEvent>> {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const e = queue.shift();
|
|
28
|
+
if (e) return { value: e, done: false };
|
|
29
|
+
if (terminal) { unsub(); return { value: undefined, done: true }; }
|
|
30
|
+
await new Promise<void>((resolve) => { notify = resolve; });
|
|
31
|
+
notify = undefined;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async return(): Promise<IteratorResult<TurnEvent>> {
|
|
35
|
+
unsub();
|
|
36
|
+
terminal = true;
|
|
37
|
+
queue.length = 0;
|
|
38
|
+
return { value: undefined, done: true };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The generated web app manifest (/manifest.webmanifest) — Android's "Add to
|
|
2
|
+
// Home Screen" name, icons, and colours, built from the same config the
|
|
3
|
+
// document renders with. Worker-safe (no node:*): the pipeline serves it like
|
|
4
|
+
// the SVG favicon, so dev, the worker, and the static build all agree.
|
|
5
|
+
import { siteShortName } from "@junejs/core/config";
|
|
6
|
+
import { resolveThemeColor, type DocumentConfig } from "@junejs/core/document";
|
|
7
|
+
|
|
8
|
+
// The home-screen label: site.shortName, else the site name's leading segment
|
|
9
|
+
// ("June — build agents into real apps" → "June"). Launchers truncate past
|
|
10
|
+
// ~12 characters, and a tagline is never the label anyone wants.
|
|
11
|
+
export const shortName = (site: DocumentConfig["site"]): string | undefined => siteShortName(site);
|
|
12
|
+
|
|
13
|
+
export function webManifest(config: DocumentConfig): string {
|
|
14
|
+
const base = config.basePath ?? "";
|
|
15
|
+
const theme = resolveThemeColor(config);
|
|
16
|
+
// A manifest has ONE theme colour; a { light, dark } pair has no honest single
|
|
17
|
+
// value, so both colour fields are left to the browser then.
|
|
18
|
+
const color = typeof theme === "string" ? theme : undefined;
|
|
19
|
+
const manifest = {
|
|
20
|
+
name: config.site.name,
|
|
21
|
+
short_name: shortName(config.site),
|
|
22
|
+
description: config.site.description,
|
|
23
|
+
start_url: `${base}/`,
|
|
24
|
+
scope: `${base}/`,
|
|
25
|
+
// "browser": a home-screen shortcut that opens a normal tab. standalone
|
|
26
|
+
// (app-like, no URL bar) changes how a site behaves, so it stays opt-in via
|
|
27
|
+
// your own public/manifest.webmanifest.
|
|
28
|
+
display: "browser",
|
|
29
|
+
...(color ? { theme_color: color, background_color: color } : {}),
|
|
30
|
+
icons: [
|
|
31
|
+
{ src: `${base}/icon-192.png`, sizes: "192x192", type: "image/png" },
|
|
32
|
+
{ src: `${base}/icon-512.png`, sizes: "512x512", type: "image/png" },
|
|
33
|
+
// The same full-bleed art: its glyph sits inside the maskable safe zone.
|
|
34
|
+
{ src: `${base}/icon-512.png`, sizes: "512x512", type: "image/png", purpose: "maskable" },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
38
|
+
}
|