@junejs/server 1.0.0-dev.42 → 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/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). Matched after pages, so a page never gets shadowed.
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
- type Compiled = { regex: RegExp; names: string[]; def: BrandedRoute; pattern: string };
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
- const oca = seg.match(/^\[\[\.\.\.(\w+)\]\]$/);
97
- if (oca) {
98
- names.push(oca[1]!);
99
- source += "(?:/(.+))?";
100
- continue;
101
- }
102
- const ca = seg.match(/^\[\.\.\.(\w+)\]$/);
103
- if (ca) {
104
- names.push(ca[1]!);
105
- source += "/(.+)";
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
- const dynamic: Compiled[] = (manifest.dynamicRoutes ?? []).map((d) => ({
136
- ...compilePattern(d.pattern),
137
- def: d.def,
138
- pattern: d.pattern,
139
- }));
140
- const resources = (manifest.resourceRoutes ?? []).map((r) => ({
141
- ...compilePattern(r.pattern),
142
- handler: r.handler,
143
- pattern: r.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
- const b = boundaryFor(pathname);
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
- const params = Object.fromEntries(
266
- r.names.flatMap((n, i) => {
267
- const v = m[i + 1];
268
- return v === undefined ? [] : [[n, decodeURIComponent(v)]];
269
- }),
270
- );
271
- return { handler: r.handler, params };
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 };