@junejs/server 1.0.0-dev.36 → 1.0.0-dev.45
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/agent-durable.d.ts +19 -2
- package/dist/agent-durable.js +118 -7
- package/dist/agent-native.d.ts +13 -2
- package/dist/agent-native.js +108 -6
- package/dist/app.d.ts +1 -0
- package/dist/app.js +10 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.js +8 -2
- package/dist/{config-freeze-ZWC_Qd4h.d.ts → config-freeze-CyXvNqi2.d.ts} +1 -1
- package/dist/config-freeze.d.ts +1 -1
- package/dist/core-version.js +1 -1
- package/dist/dev.d.ts +6 -2
- package/dist/dev.js +40 -9
- package/dist/host.d.ts +1 -0
- package/dist/host.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{manifest-DSNX4I-G.d.ts → manifest-CwTT1zVs.d.ts} +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +3 -1
- package/dist/pipeline.js +15 -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 -2
- package/dist/route-scan.js +47 -2
- package/dist/router.js +58 -44
- package/dist/{worker-Dv4l6Ybs.d.ts → worker-2qbQjj2c.d.ts} +1 -0
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +52 -76
- package/package.json +3 -3
- package/src/agent-durable.ts +164 -11
- package/src/agent-native.ts +161 -7
- package/src/app.ts +22 -1
- package/src/build.ts +11 -1
- package/src/core-version.ts +1 -1
- package/src/dev.ts +41 -8
- package/src/host.ts +4 -3
- package/src/manifest.ts +2 -0
- package/src/pipeline.ts +20 -4
- package/src/route-rank.ts +60 -0
- package/src/route-scan.ts +78 -1
- package/src/router.ts +75 -58
- package/src/worker.ts +66 -82
- package/dist/route-scan-DMlwoESy.d.ts +0 -13
package/src/route-scan.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { readdir } from "node:fs/promises";
|
|
9
9
|
import { basename, join, relative, sep } from "node:path";
|
|
10
10
|
|
|
11
|
+
import { parseSegment } from "./route-rank";
|
|
12
|
+
|
|
11
13
|
// The segment layout CHAIN root→leaf: every directory level (route groups
|
|
12
14
|
// included) may contribute a layout.* that wraps routes below it.
|
|
13
15
|
export type RouteEntry = {
|
|
@@ -15,6 +17,7 @@ export type RouteEntry = {
|
|
|
15
17
|
file: string;
|
|
16
18
|
dynamic: boolean;
|
|
17
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
|
|
18
21
|
layouts: string[];
|
|
19
22
|
loading?: string; // nearest loading.tsx up the tree → streaming Suspense fallback
|
|
20
23
|
};
|
|
@@ -77,8 +80,82 @@ export async function scanAppRoutes(appRoot: string): Promise<RouteEntry[]> {
|
|
|
77
80
|
const juneRoutesDir = join(appRoot, ".june", "routes");
|
|
78
81
|
const appRoutes = await scanRoutes(appDir);
|
|
79
82
|
const frameworkRoutes = existsSync(juneRoutesDir) ? await scanRoutes(juneRoutesDir) : [];
|
|
83
|
+
const problems = routeProblems([appRoutes, frameworkRoutes], appRoot);
|
|
84
|
+
if (problems) throw new Error(problems);
|
|
80
85
|
const appPaths = new Set(appRoutes.map((r) => r.path));
|
|
81
|
-
|
|
86
|
+
const generated = frameworkRoutes
|
|
87
|
+
.filter((r) => !appPaths.has(r.path))
|
|
88
|
+
.map((r) => ({ ...r, generated: true }));
|
|
89
|
+
return [...appRoutes, ...generated].sort((a, b) =>
|
|
82
90
|
a.path.localeCompare(b.path),
|
|
83
91
|
);
|
|
84
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
|
@@ -6,6 +6,8 @@ import { existsSync } from "node:fs";
|
|
|
6
6
|
import { readdir } from "node:fs/promises";
|
|
7
7
|
import { join, relative, sep } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { compareSegments, parseSegment } from "./route-rank";
|
|
10
|
+
|
|
9
11
|
export type RouteMatch = {
|
|
10
12
|
file: string;
|
|
11
13
|
params: Record<string, string>;
|
|
@@ -87,13 +89,13 @@ function isSpecialFile(file: string) {
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
const isRouteGroup = (name: string) => /^\(.+\)$/.test(name);
|
|
90
|
-
const isParamDir = (name: string) =>
|
|
92
|
+
const isParamDir = (name: string) => parseSegment(name).kind === "param";
|
|
91
93
|
// [[slug]] / [[...slug]] — match like their required forms, and ALSO match the
|
|
92
94
|
// segment being absent (the param is then simply missing from ctx.params).
|
|
93
|
-
const isOptionalDir = (name: string) =>
|
|
94
|
-
const isOptionalCatchAllDir = (name: string) =>
|
|
95
|
-
const isCatchAllDir = (name: string) =>
|
|
96
|
-
const paramName = (name: string) => name.
|
|
95
|
+
const isOptionalDir = (name: string) => parseSegment(name).kind === "optional";
|
|
96
|
+
const isOptionalCatchAllDir = (name: string) => parseSegment(name).kind === "optionalCatchAll";
|
|
97
|
+
const isCatchAllDir = (name: string) => parseSegment(name).kind === "catchAll";
|
|
98
|
+
const paramName = (name: string) => parseSegment(name).name!;
|
|
97
99
|
|
|
98
100
|
type DirEntry = { name: string; dir: boolean };
|
|
99
101
|
|
|
@@ -147,8 +149,12 @@ function routePath(appDir: string, file: string) {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
// Recursive-descent matcher over the app directory. Priority at every level:
|
|
150
|
-
// exact static segment > [param] > [...catchAll]
|
|
151
|
-
//
|
|
152
|
+
// exact static segment > [param] > [...catchAll], required before optional, ties
|
|
153
|
+
// by name (route-rank.ts — the built worker sorts its route table by the same
|
|
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.
|
|
152
158
|
// Returns the page file, accumulated params (catch-all joins with "/"), and the
|
|
153
159
|
// chain of segments (with their special files) from the app root to the page.
|
|
154
160
|
export async function matchRouteTree(
|
|
@@ -158,85 +164,96 @@ export async function matchRouteTree(
|
|
|
158
164
|
): Promise<RouteTreeMatch | null> {
|
|
159
165
|
const urlSegments = pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
160
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
|
+
|
|
161
186
|
async function descend(
|
|
162
|
-
|
|
187
|
+
nodes: Node[],
|
|
163
188
|
rest: string[],
|
|
164
189
|
params: Record<string, string>,
|
|
165
|
-
chain: SegmentMatch[],
|
|
166
190
|
): Promise<RouteTreeMatch | null> {
|
|
167
|
-
const
|
|
168
|
-
const segments = [...chain, segmentAt(dir, entries)];
|
|
191
|
+
const levels = (await Promise.all(nodes.map((n) => levelAt(n.dir, n.chain)))).flat();
|
|
169
192
|
|
|
170
|
-
// 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).
|
|
171
196
|
if (rest.length === 0) {
|
|
172
|
-
const page
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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) {
|
|
178
204
|
// Legacy flat convention: a non-special leaf FILE names the final segment
|
|
179
205
|
// (examples/rsc: about.tsx → /about). Only valid for the last segment.
|
|
180
|
-
|
|
181
|
-
const leaf = fileFor(entries,
|
|
182
|
-
if (leaf && !isSpecialFile(leaf) && !isPageFile(leaf)) {
|
|
183
|
-
return { file: leaf, params, segments };
|
|
184
|
-
}
|
|
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 };
|
|
185
209
|
}
|
|
186
210
|
}
|
|
187
211
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
}
|
|
193
223
|
}
|
|
224
|
+
const names = [...children.keys()].sort(compareSegments);
|
|
194
225
|
|
|
195
226
|
if (rest.length === 0) {
|
|
196
227
|
// Optional segments match ABSENCE too: descend without consuming and
|
|
197
228
|
// without setting the param.
|
|
198
|
-
for (const
|
|
199
|
-
if (!
|
|
200
|
-
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);
|
|
201
232
|
if (hit) return hit;
|
|
202
233
|
}
|
|
203
234
|
return null;
|
|
204
235
|
}
|
|
205
236
|
const [head, ...tail] = rest as [string, ...string[]];
|
|
206
237
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
{ ...params, [paramName(e.name)]: head },
|
|
220
|
-
segments,
|
|
221
|
-
);
|
|
222
|
-
if (hit) return hit;
|
|
223
|
-
}
|
|
224
|
-
// 3) [...catchAll] and [[...catchAll]] dirs consume everything remaining
|
|
225
|
-
for (const e of entries) {
|
|
226
|
-
if (!e.dir || !(isCatchAllDir(e.name) || isOptionalCatchAllDir(e.name))) continue;
|
|
227
|
-
const hit = await descend(
|
|
228
|
-
join(dir, e.name),
|
|
229
|
-
[],
|
|
230
|
-
{ ...params, [paramName(e.name)]: rest.join("/") },
|
|
231
|
-
segments,
|
|
232
|
-
);
|
|
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
|
+
}
|
|
233
250
|
if (hit) return hit;
|
|
234
251
|
}
|
|
235
252
|
|
|
236
253
|
return null;
|
|
237
254
|
}
|
|
238
255
|
|
|
239
|
-
return descend(appDir, urlSegments, {}
|
|
256
|
+
return descend([{ dir: appDir, chain: [] }], urlSegments, {});
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
// 404 path: walk the longest matchable STATIC prefix of the URL collecting
|
package/src/worker.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { Channel, ChannelFactory } from "@junejs/core/agent-config";
|
|
|
22
22
|
import { createPipeline, type ExtraHandler, type LayoutComponent, type LoadingComponent, type Resolved, type ResolvedResource, type ResourceHandler } from "./pipeline";
|
|
23
23
|
import { durableAgentSurface, durableChannelSurface, type DurableObjectNamespace } from "./agent-durable";
|
|
24
24
|
import { acceptTarget } from "./negotiate";
|
|
25
|
+
import { compareRoutePatterns, parseSegment } from "./route-rank";
|
|
25
26
|
import { contentTypeFor, RESERVED_PREFIX, safeRelativePath } from "./static-files";
|
|
26
27
|
|
|
27
28
|
// The slice of workerd's ExecutionContext the worker threads through: webhook
|
|
@@ -37,8 +38,13 @@ export type WorkerManifest = {
|
|
|
37
38
|
// Dynamic patterns in file-route syntax ("/posts/[slug]", "/docs/[...path]").
|
|
38
39
|
dynamicRoutes?: Array<{ pattern: string; def: BrandedRoute }>;
|
|
39
40
|
// Resource routes (app/**/route.*): a raw-Response handler keyed by its pattern
|
|
40
|
-
// (static or dynamic).
|
|
41
|
+
// (static or dynamic). Ranked with the pages by segment shape (route-rank.ts),
|
|
42
|
+
// exactly as `june dev` resolves; a page wins only at the SAME pattern.
|
|
41
43
|
resourceRoutes?: Array<{ pattern: string; handler: ResourceHandler }>;
|
|
44
|
+
// Patterns scanned from .june/routes/ (framework-generated, e.g. Kura). Dev
|
|
45
|
+
// consults app/ first and falls back to .june/routes/, so every one of these
|
|
46
|
+
// ranks after every app/ route. Absent → all routes are app/ routes.
|
|
47
|
+
generatedRoutes?: string[];
|
|
42
48
|
// Layout chains (root→leaf) keyed by route path / dynamic pattern. The build
|
|
43
49
|
// freezes the same chain the dev server loads from app/layout.* files.
|
|
44
50
|
layoutChains?: Record<string, LayoutComponent[]>;
|
|
@@ -83,7 +89,13 @@ export type WorkerManifest = {
|
|
|
83
89
|
services?: (env?: unknown) => unknown;
|
|
84
90
|
};
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
// One row of the worker's route table: a page (def) or a resource route (handler).
|
|
93
|
+
type Compiled = {
|
|
94
|
+
regex: RegExp;
|
|
95
|
+
names: string[];
|
|
96
|
+
pattern: string;
|
|
97
|
+
generated: boolean;
|
|
98
|
+
} & ({ def: BrandedRoute; handler?: undefined } | { def?: undefined; handler: ResourceHandler });
|
|
87
99
|
|
|
88
100
|
// "/posts/[slug]" | "/docs/[...path]" | "/notes/[[tag]]" → matcher. Optional
|
|
89
101
|
// segments ([[x]], [[...x]]) wrap slash + capture together so absence matches;
|
|
@@ -93,34 +105,19 @@ function compilePattern(pattern: string): { regex: RegExp; names: string[] } {
|
|
|
93
105
|
let source = "";
|
|
94
106
|
let allOptional = true;
|
|
95
107
|
for (const seg of pattern.split("/").filter(Boolean)) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
// Same grammar as the dev matcher (route-rank.ts parseSegment).
|
|
109
|
+
const { kind, name } = parseSegment(seg);
|
|
110
|
+
if (name) names.push(name);
|
|
111
|
+
if (kind === "optionalCatchAll") source += "(?:/(.+))?";
|
|
112
|
+
else if (kind === "optional") source += "(?:/([^/]+))?";
|
|
113
|
+
else {
|
|
114
|
+
if (kind === "catchAll") source += "/(.+)";
|
|
115
|
+
else if (kind === "param") source += "/([^/]+)";
|
|
116
|
+
// A static segment matches literally: escape every RegExp metacharacter,
|
|
117
|
+
// brackets included (`docs[v2`, `[slug].png` are static names).
|
|
118
|
+
else source += "/" + seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
106
119
|
allOptional = false;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const op = seg.match(/^\[\[(\w+)\]\]$/);
|
|
110
|
-
if (op) {
|
|
111
|
-
names.push(op[1]!);
|
|
112
|
-
source += "(?:/([^/]+))?";
|
|
113
|
-
continue;
|
|
114
120
|
}
|
|
115
|
-
const p = seg.match(/^\[(\w+)\]$/);
|
|
116
|
-
if (p) {
|
|
117
|
-
names.push(p[1]!);
|
|
118
|
-
source += "/([^/]+)";
|
|
119
|
-
allOptional = false;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
source += "/" + seg.replace(/[.*+?^${}()|\\]/g, "\\$&");
|
|
123
|
-
allOptional = false;
|
|
124
121
|
}
|
|
125
122
|
if (source === "") return { regex: /^\/$/, names };
|
|
126
123
|
return { regex: new RegExp(allOptional ? `^(?:${source}|/)$` : `^${source}$`), names };
|
|
@@ -132,16 +129,23 @@ export function createWorker(
|
|
|
132
129
|
manifest: WorkerManifest,
|
|
133
130
|
opts: { staticBuild?: boolean } = {},
|
|
134
131
|
): { fetch(request: Request, env?: unknown, ctx?: WorkerExecutionContext): Promise<Response> } {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
pattern:
|
|
144
|
-
|
|
132
|
+
// Pages and resource routes in ONE table, ordered the way `june dev` resolves
|
|
133
|
+
// (#312): app/ before .june/routes/, then segment shape (static > [param] >
|
|
134
|
+
// [...catchAll], required before optional), then a page before a resource
|
|
135
|
+
// route at the same pattern. The kind of route never outranks the shape.
|
|
136
|
+
const generated = new Set(manifest.generatedRoutes ?? []);
|
|
137
|
+
const row = (pattern: string) => ({ ...compilePattern(pattern), pattern, generated: generated.has(pattern) });
|
|
138
|
+
const table: Compiled[] = [
|
|
139
|
+
...Object.entries(manifest.routes).map(([pattern, def]) => ({ ...row(pattern), def })),
|
|
140
|
+
...(manifest.dynamicRoutes ?? []).map((d) => ({ ...row(d.pattern), def: d.def })),
|
|
141
|
+
...(manifest.resourceRoutes ?? []).map((r) => ({ ...row(r.pattern), handler: r.handler })),
|
|
142
|
+
];
|
|
143
|
+
table.sort(
|
|
144
|
+
(a, b) =>
|
|
145
|
+
Number(a.generated) - Number(b.generated) ||
|
|
146
|
+
compareRoutePatterns(a.pattern, b.pattern) ||
|
|
147
|
+
Number(!!a.handler) - Number(!!b.handler),
|
|
148
|
+
);
|
|
145
149
|
const routeList = [
|
|
146
150
|
...Object.keys(manifest.routes),
|
|
147
151
|
...(manifest.dynamicRoutes ?? []).map((d) => d.pattern),
|
|
@@ -151,6 +155,17 @@ export function createWorker(
|
|
|
151
155
|
const loadingFor = (key: string): LoadingComponent | undefined => manifest.loadings?.[key];
|
|
152
156
|
const boundaryFor = (key: string): { index: number; key: string } | null =>
|
|
153
157
|
manifest.layoutBoundaries?.[key] ?? null;
|
|
158
|
+
const resolvedPage = (def: BrandedRoute, key: string, params: Record<string, string>): Resolved => {
|
|
159
|
+
const b = boundaryFor(key);
|
|
160
|
+
return {
|
|
161
|
+
def,
|
|
162
|
+
params,
|
|
163
|
+
chain: chainFor(key),
|
|
164
|
+
boundaryIndex: b?.index ?? null,
|
|
165
|
+
boundaryKey: b?.key ?? null,
|
|
166
|
+
loading: loadingFor(key),
|
|
167
|
+
};
|
|
168
|
+
};
|
|
154
169
|
|
|
155
170
|
// The worker's env (D1/KV/R2 bindings) arrives per fetch and is stable across
|
|
156
171
|
// requests in an isolate; we capture the latest and hand it to the env-aware
|
|
@@ -224,52 +239,21 @@ export function createWorker(
|
|
|
224
239
|
resources: provider ? () => provider(currentEnv) : undefined,
|
|
225
240
|
services: servicesProvider ? () => (servicesCache ??= { v: servicesProvider(currentEnv) }).v : undefined,
|
|
226
241
|
resolve: async (pathname): Promise<Resolved | ResolvedResource | null> => {
|
|
242
|
+
// Fast path: an app/ static page outranks every other route for its path.
|
|
227
243
|
const staticDef = manifest.routes[pathname];
|
|
228
|
-
if (staticDef) {
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
def: staticDef,
|
|
232
|
-
params: {},
|
|
233
|
-
chain: chainFor(pathname),
|
|
234
|
-
boundaryIndex: b?.index ?? null,
|
|
235
|
-
boundaryKey: b?.key ?? null,
|
|
236
|
-
loading: loadingFor(pathname),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
for (const d of dynamic) {
|
|
240
|
-
const m = pathname.match(d.regex);
|
|
241
|
-
if (m) {
|
|
242
|
-
// Optional segments leave their capture undefined — the param is
|
|
243
|
-
// then absent, matching the dev matcher's semantics.
|
|
244
|
-
const params = Object.fromEntries(
|
|
245
|
-
d.names.flatMap((n, i) => {
|
|
246
|
-
const v = m[i + 1];
|
|
247
|
-
return v === undefined ? [] : [[n, decodeURIComponent(v)]];
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
const b = boundaryFor(d.pattern);
|
|
251
|
-
return {
|
|
252
|
-
def: d.def,
|
|
253
|
-
params,
|
|
254
|
-
chain: chainFor(d.pattern),
|
|
255
|
-
boundaryIndex: b?.index ?? null,
|
|
256
|
-
boundaryKey: b?.key ?? null,
|
|
257
|
-
loading: loadingFor(d.pattern),
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Resource routes after pages (so a page is never shadowed by one).
|
|
262
|
-
for (const r of resources) {
|
|
244
|
+
if (staticDef && !generated.has(pathname)) return resolvedPage(staticDef, pathname, {});
|
|
245
|
+
for (const r of table) {
|
|
263
246
|
const m = pathname.match(r.regex);
|
|
264
|
-
if (m)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
247
|
+
if (!m) continue;
|
|
248
|
+
// Optional segments leave their capture undefined — the param is
|
|
249
|
+
// then absent, matching the dev matcher's semantics.
|
|
250
|
+
const params = Object.fromEntries(
|
|
251
|
+
r.names.flatMap((n, i) => {
|
|
252
|
+
const v = m[i + 1];
|
|
253
|
+
return v === undefined ? [] : [[n, decodeURIComponent(v)]];
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
return r.handler ? { handler: r.handler, params } : resolvedPage(r.def, r.pattern, params);
|
|
273
257
|
}
|
|
274
258
|
return null;
|
|
275
259
|
},
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region src/route-scan.d.ts
|
|
2
|
-
type RouteEntry = {
|
|
3
|
-
path: string;
|
|
4
|
-
file: string;
|
|
5
|
-
dynamic: boolean;
|
|
6
|
-
resource?: boolean;
|
|
7
|
-
layouts: string[];
|
|
8
|
-
loading?: string;
|
|
9
|
-
};
|
|
10
|
-
declare function scanRoutes(appDir: string, dir?: string, layouts?: string[], out?: RouteEntry[], loading?: string): Promise<RouteEntry[]>;
|
|
11
|
-
declare function scanAppRoutes(appRoot: string): Promise<RouteEntry[]>;
|
|
12
|
-
//#endregion
|
|
13
|
-
export { scanAppRoutes as n, scanRoutes as r, RouteEntry as t };
|