@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.
@@ -19,11 +19,13 @@ export declare class NativeRuntime implements Runtime {
19
19
  private readonly db;
20
20
  private readonly announceRetry;
21
21
  private readonly maxSessions;
22
+ private closing;
22
23
  constructor(agents: Record<string, AgentDef>, db: SyncSqlite, opts?: NativeRuntimeOptions);
23
24
  agentDef(name: string): AgentDef | undefined;
24
25
  session(agent: string, id: string): AgentSession;
25
26
  get sessionCount(): number;
26
27
  recoverAnnouncements(): void;
28
+ close(): Promise<void>;
27
29
  private evictIdle;
28
30
  }
29
31
  export declare function createNativeRuntime(agents: Record<string, AgentDef>, path?: string, opts?: NativeRuntimeOptions): Promise<NativeRuntime>;
@@ -32,16 +34,21 @@ export declare class MemoryRuntime implements Runtime {
32
34
  private stores;
33
35
  private readonly agents;
34
36
  private readonly announceRetry;
37
+ private closed;
35
38
  constructor(agents: Record<string, AgentDef>);
36
39
  agentDef(name: string): AgentDef | undefined;
37
40
  session(agent: string, id: string): AgentSession;
41
+ close(): Promise<void>;
38
42
  }
39
43
  export type AgentBackend = "native" | "memory" | "durable";
44
+ export type InProcessRuntime = Runtime & {
45
+ close(): Promise<void>;
46
+ };
40
47
  export declare function createAgentRuntime(agents: Record<string, AgentDef>, opts?: {
41
48
  backend?: AgentBackend;
42
49
  path?: string;
43
50
  maxSessions?: number;
44
- }): Promise<Runtime>;
51
+ }): Promise<InProcessRuntime>;
45
52
  export declare function mountAgent(agent: AgentDefinition, runtime: Runtime, opts?: {
46
53
  chatPath?: string;
47
54
  channels?: boolean;
@@ -93,6 +93,7 @@ var InProcEventSink = class {
93
93
  var AnnouncementRetry = class {
94
94
  flush;
95
95
  state = /* @__PURE__ */ new Map();
96
+ stopped = false;
96
97
  constructor(flush) {
97
98
  this.flush = flush;
98
99
  }
@@ -102,6 +103,7 @@ var AnnouncementRetry = class {
102
103
  this.state.delete(key);
103
104
  }
104
105
  failed(key, agent, id) {
106
+ if (this.stopped) return;
105
107
  const s = this.state.get(key) ?? { failures: 0 };
106
108
  this.state.set(key, s);
107
109
  if (s.timer) return;
@@ -113,7 +115,13 @@ var AnnouncementRetry = class {
113
115
  }, delay);
114
116
  s.timer.unref?.();
115
117
  }
118
+ stop() {
119
+ this.stopped = true;
120
+ for (const s of this.state.values()) if (s.timer) clearTimeout(s.timer);
121
+ this.state.clear();
122
+ }
116
123
  };
124
+ const ownedDbs = /* @__PURE__ */ new WeakSet();
117
125
  function wireAnnouncements(session, def, retry) {
118
126
  const hook = def.onInputAnnouncement;
119
127
  if (hook) {
@@ -149,6 +157,7 @@ var NativeRuntime = class {
149
157
  this.session(agent, id).flushAnnouncements();
150
158
  });
151
159
  maxSessions;
160
+ closing;
152
161
  constructor(agents, db, opts = {}) {
153
162
  assertCoreRuntimeVersion("NativeRuntime");
154
163
  this.agents = agents;
@@ -164,6 +173,7 @@ var NativeRuntime = class {
164
173
  session(agent, id) {
165
174
  const key = `${agent}:${id}`;
166
175
  const hit = this.actors.get(key);
176
+ if (!hit && this.closing) throw new Error(`NativeRuntime is closed: cannot open session "${key}"`);
167
177
  if (hit) {
168
178
  this.actors.delete(key);
169
179
  this.actors.set(key, hit);
@@ -191,6 +201,18 @@ var NativeRuntime = class {
191
201
  if (first && this.agents[first.agent]?.onInputAnnouncement) this.session(first.agent, first.session);
192
202
  }
193
203
  }
204
+ close() {
205
+ this.closing ??= (async () => {
206
+ this.announceRetry.stop();
207
+ while ([...this.actors.values()].some((a) => !a.session.idle())) await new Promise((r) => setTimeout(r, 10));
208
+ this.actors.clear();
209
+ if (ownedDbs.has(this.db)) {
210
+ ownedDbs.delete(this.db);
211
+ this.db.close();
212
+ }
213
+ })();
214
+ return this.closing;
215
+ }
194
216
  evictIdle() {
195
217
  if (this.actors.size < this.maxSessions) return;
196
218
  for (const [key, a] of this.actors) {
@@ -200,9 +222,21 @@ var NativeRuntime = class {
200
222
  }
201
223
  };
202
224
  async function createNativeRuntime(agents, path = ":memory:", opts = {}) {
203
- const runtime = new NativeRuntime(agents, await openLocalSqliteSync(path), opts);
204
- runtime.recoverAnnouncements();
205
- return runtime;
225
+ const db = await openLocalSqliteSync(path);
226
+ ownedDbs.add(db);
227
+ let runtime;
228
+ try {
229
+ runtime = new NativeRuntime(agents, db, opts);
230
+ runtime.recoverAnnouncements();
231
+ return runtime;
232
+ } catch (err) {
233
+ if (runtime) await runtime.close();
234
+ else {
235
+ ownedDbs.delete(db);
236
+ db.close();
237
+ }
238
+ throw err;
239
+ }
206
240
  }
207
241
  var MemorySessionStore = class {
208
242
  msgs = [];
@@ -259,6 +293,7 @@ var MemoryRuntime = class {
259
293
  announceRetry = new AnnouncementRetry((agent, id) => {
260
294
  this.session(agent, id).flushAnnouncements();
261
295
  });
296
+ closed = false;
262
297
  constructor(agents) {
263
298
  assertCoreRuntimeVersion("MemoryRuntime");
264
299
  this.agents = agents;
@@ -270,6 +305,7 @@ var MemoryRuntime = class {
270
305
  const key = `${agent}:${id}`;
271
306
  let a = this.actors.get(key);
272
307
  if (!a) {
308
+ if (this.closed) throw new Error(`MemoryRuntime is closed: cannot open session "${key}"`);
273
309
  const def = this.agents[agent];
274
310
  if (!def) throw new Error(`unknown agent: ${agent}`);
275
311
  const store = new MemorySessionStore();
@@ -280,6 +316,12 @@ var MemoryRuntime = class {
280
316
  }
281
317
  return a;
282
318
  }
319
+ async close() {
320
+ this.closed = true;
321
+ this.announceRetry.stop();
322
+ this.actors.clear();
323
+ this.stores.clear();
324
+ }
283
325
  };
284
326
  async function createAgentRuntime(agents, opts = {}) {
285
327
  const backend = opts.backend ?? "native";
package/dist/app.d.ts CHANGED
@@ -9,6 +9,7 @@ export type JuneApp = {
9
9
  warmup(): Promise<void>;
10
10
  routePaths(): Promise<string[]>;
11
11
  earlyHints(): string[];
12
+ close(): Promise<void>;
12
13
  };
13
14
  export declare function createApp({ appDir: appDirInput, config }: CreateAppOptions): JuneApp;
14
15
  //#endregion
package/dist/app.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { discoverAgent } from "./agent-discover.js";
2
2
  import { createAgentRuntime, mountAgent, toAgentDef } from "./agent-native.js";
3
3
  import { findMiddlewareFile, isResourceFile, listRoutes, matchRouteTree, resolveNotFound, routeFiles } from "./router.js";
4
+ import { routeProblems, scanRoutes } from "./route-scan.js";
4
5
  import { createPipeline } from "./pipeline.js";
5
6
  import { resolveBoundary } from "./segment.js";
6
7
  import { memoizeResources } from "./resources.js";
@@ -115,6 +116,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
115
116
  config.dataLayer?.install();
116
117
  let pipelinePromise;
117
118
  const getPipeline = () => pipelinePromise ??= buildPipeline();
119
+ let agentRuntime;
118
120
  async function buildPipeline() {
119
121
  const { maps, css } = await getModuleCss();
120
122
  await registerCssModules(maps);
@@ -138,6 +140,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
138
140
  const backend = agent.runtime.backend === "durable" ? "native" : agent.runtime.backend;
139
141
  const model = anthropic({ model: def.model });
140
142
  const rt = await createAgentRuntime({ [def.name]: toAgentDef(def, model) }, { backend });
143
+ agentRuntime = rt;
141
144
  const mounted = mountAgent(def, rt, {
142
145
  chatPath: agent.runtime.chat.path,
143
146
  channels: agent.runtime.channels
@@ -257,6 +260,8 @@ function createApp({ appDir: appDirInput, config = {} }) {
257
260
  return toPipeline();
258
261
  },
259
262
  async warmup() {
263
+ const problems = routeProblems([await scanRoutes(appDir), hasJuneRoutes ? await scanRoutes(juneRoutesDir) : []], dirname(appDir));
264
+ if (problems) console.error(problems);
260
265
  const files = await routeFiles(appDir, { pageConvention: true });
261
266
  if (hasJuneRoutes) files.push(...await routeFiles(juneRoutesDir, { pageConvention: true }));
262
267
  for (const file of files) await import(pathToFileURL(file).href).catch((err) => {
@@ -264,7 +269,11 @@ function createApp({ appDir: appDirInput, config = {} }) {
264
269
  });
265
270
  },
266
271
  routePaths,
267
- earlyHints: () => config.earlyHints ?? []
272
+ earlyHints: () => config.earlyHints ?? [],
273
+ async close() {
274
+ await pipelinePromise?.catch(() => {});
275
+ await agentRuntime?.close();
276
+ }
268
277
  };
269
278
  }
270
279
  //#endregion
package/dist/build.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { n as scanAppRoutes, r as scanRoutes, t as RouteEntry } from "./route-scan-DMlwoESy.js";
1
+ import { a as scanAppRoutes, o as scanRoutes, t as RouteEntry } from "./route-scan-BMbBzPIB.js";
2
2
  import { t as generateContent } from "./content-freeze-CyiW2fRK.js";
3
- import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-ZWC_Qd4h.js";
4
- import { n as buildManifest } from "./manifest-DSNX4I-G.js";
3
+ import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-CyXvNqi2.js";
4
+ import { n as buildManifest } from "./manifest-CwTT1zVs.js";
5
5
  //#region src/build.d.ts
6
6
  export type BuildResult = {
7
7
  outFile: string;
package/dist/build.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsxTransform } from "./tsconfig-jsx.js";
2
2
  import { generateAgentModule } from "./agent-compile.js";
3
3
  import { findMiddlewareFile } from "./router.js";
4
+ import { scanAppRoutes, scanRoutes } from "./route-scan.js";
4
5
  import { resolveBoundary } from "./segment.js";
5
6
  import { buildModuleCss, registerCssModules, rolldownCssModulesPlugin } from "./css-modules.js";
6
7
  import { bundleClientToFile, findClientEntry } from "./client-bundle.js";
@@ -12,7 +13,6 @@ import { loadJuneConfig } from "./config-loader.js";
12
13
  import { freezeConfig, normalizeBase, resolveDeployAdapter, workerName } from "./config-freeze.js";
13
14
  import { generateContent } from "./content-freeze.js";
14
15
  import { createWorker } from "./worker.js";
15
- import { scanAppRoutes, scanRoutes } from "./route-scan.js";
16
16
  import { buildManifest, importLayout } from "./manifest.js";
17
17
  import { buildRsc, findRscRoutes } from "./rsc-build.js";
18
18
  import { existsSync, lstatSync } from "node:fs";
@@ -154,6 +154,8 @@ async function juneBuild(appRoot, options = {}) {
154
154
  if (r.loading) loadings.push(` ${JSON.stringify(r.path)}: ${loadingId(r.loading)},`);
155
155
  });
156
156
  const resourceRoutesField = resources.length ? `\n resourceRoutes: [\n${resources.join("\n")}\n ],` : "";
157
+ const generatedPaths = routes.filter((r) => r.generated).map((r) => r.path);
158
+ const generatedRoutesField = generatedPaths.length ? `\n generatedRoutes: ${JSON.stringify(generatedPaths)},` : "";
157
159
  const layoutBoundariesField = boundaries.length ? `\n layoutBoundaries: {\n${boundaries.join("\n")}\n },` : "";
158
160
  const builtExtraFile = findMiddlewareFile(appDir);
159
161
  if (builtExtraFile) imports.push(`import extra from ${JSON.stringify(importPath(genDir, builtExtraFile))};`);
@@ -224,7 +226,7 @@ ${statics.join("\n")}
224
226
  },
225
227
  dynamicRoutes: [
226
228
  ${dynamics.join("\n")}
227
- ],${resourceRoutesField}
229
+ ],${resourceRoutesField}${generatedRoutesField}
228
230
  layoutChains: {
229
231
  ${chains.join("\n")}
230
232
  },${layoutBoundariesField}
@@ -1,5 +1,5 @@
1
1
  import { i as JuneAdapter } from "./adapter-DcZ-W1-7.js";
2
- import { n as WorkerManifest } from "./worker-Dv4l6Ybs.js";
2
+ import { n as WorkerManifest } from "./worker-2qbQjj2c.js";
3
3
  import { JuneConfig } from "@junejs/core/config";
4
4
  import { DocumentConfig } from "@junejs/core/document";
5
5
  //#region src/config-freeze.d.ts
@@ -1,2 +1,2 @@
1
- import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-ZWC_Qd4h.js";
1
+ import { i as workerName, n as normalizeBase, r as resolveDeployAdapter, t as freezeConfig } from "./config-freeze-CyXvNqi2.js";
2
2
  export { freezeConfig, normalizeBase, resolveDeployAdapter, workerName };
package/dist/dev.d.ts CHANGED
@@ -3,10 +3,14 @@ import { JuneHost, ServeHandle } from "./host.js";
3
3
  export type DevServerOptions = {
4
4
  appDir: string;
5
5
  port?: number;
6
+ hostname?: string;
6
7
  host?: JuneHost;
7
8
  };
8
- export type DevServer = ServeHandle & {
9
+ export declare function devUrl(hostname: string, port: number): string;
10
+ export declare function devAllowedHosts(configured: readonly string[] | undefined, hostname: string): string[];
11
+ export type DevServer = Omit<ServeHandle, "stop"> & {
9
12
  url: string;
13
+ stop(force?: boolean): Promise<void>;
10
14
  };
11
- export declare function startDevServer({ appDir, port, host }: DevServerOptions): Promise<DevServer>;
15
+ export declare function startDevServer({ appDir, port, hostname, host }: DevServerOptions): Promise<DevServer>;
12
16
  //#endregion
package/dist/dev.js CHANGED
@@ -9,17 +9,42 @@ import { watch } from "node:fs";
9
9
  import { dirname } from "node:path";
10
10
  import { createServer } from "node:net";
11
11
  //#region src/dev.ts
12
- async function findFreePort(start, tries = 20) {
12
+ function devUrl(hostname, port) {
13
+ if ([
14
+ "127.0.0.1",
15
+ "0.0.0.0",
16
+ "::",
17
+ "localhost"
18
+ ].includes(hostname)) return `http://localhost:${port}`;
19
+ return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`;
20
+ }
21
+ function devAllowedHosts(configured, hostname) {
22
+ const hosts = /* @__PURE__ */ new Set([
23
+ "localhost",
24
+ ".localhost",
25
+ ...configured ?? []
26
+ ]);
27
+ if (!/^[\d.]+$|:/.test(hostname)) hosts.add(hostname.toLowerCase());
28
+ return [...hosts];
29
+ }
30
+ async function findFreePort(start, hostname, tries = 20) {
13
31
  for (let p = start; p < start + tries; p++) if (await new Promise((resolve) => {
14
32
  const probe = createServer();
15
33
  probe.once("error", () => resolve(false));
16
- probe.listen(p, () => probe.close(() => resolve(true)));
34
+ probe.listen(p, hostname, () => probe.close(() => resolve(true)));
17
35
  })) return p;
18
36
  throw new Error(`june dev: no free port between ${start} and ${start + tries - 1}`);
19
37
  }
20
- async function startDevServer({ appDir, port = 3e3, host: host$1 = host }) {
38
+ async function startDevServer({ appDir, port = 3e3, hostname = "127.0.0.1", host: host$1 = host }) {
21
39
  await installAsyncContext();
22
- const config = await loadJuneConfig(appDir);
40
+ const loaded = await loadJuneConfig(appDir);
41
+ const config = {
42
+ ...loaded,
43
+ agent: {
44
+ ...loaded.agent,
45
+ allowedHosts: devAllowedHosts(loaded.agent?.allowedHosts, hostname)
46
+ }
47
+ };
23
48
  const m = await migrateApp(dirname(appDir), config);
24
49
  if (m?.applied.length) console.log(`[june] migrated: ${m.applied.join(", ")}`);
25
50
  if (m?.blocked) console.warn(`[june] ${blockedMessage(m.blocked)}`);
@@ -28,11 +53,12 @@ async function startDevServer({ appDir, port = 3e3, host: host$1 = host }) {
28
53
  config
29
54
  });
30
55
  await app.warmup();
31
- const freePort = await findFreePort(port);
56
+ const freePort = await findFreePort(port, hostname);
32
57
  if (freePort !== port) console.log(`[june] port ${port} is taken → using ${freePort}`);
33
58
  port = freePort;
34
59
  const handle = host$1.serve(withLiveReload((req) => app.fetch(req)), {
35
60
  port,
61
+ hostname,
36
62
  earlyHints: () => app.earlyHints()
37
63
  });
38
64
  if (findGlobalCss(appDir)) {
@@ -44,12 +70,17 @@ async function startDevServer({ appDir, port = 3e3, host: host$1 = host }) {
44
70
  });
45
71
  processCssCached(appDir).catch(() => {});
46
72
  }
47
- const url = `http://localhost:${handle.port}`;
48
- console.log(`june dev → ${url} (host: ${host$1.name})`);
73
+ const url = devUrl(hostname, handle.port);
74
+ const lan = hostname === "127.0.0.1" ? "" : ` · listening on ${hostname}`;
75
+ console.log(`june dev → ${url} (host: ${host$1.name})${lan}`);
49
76
  return {
50
77
  ...handle,
51
- url
78
+ url,
79
+ async stop(force) {
80
+ handle.stop(force);
81
+ await app.close();
82
+ }
52
83
  };
53
84
  }
54
85
  //#endregion
55
- export { startDevServer };
86
+ export { devAllowedHosts, devUrl, startDevServer };
package/dist/host.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface JuneHost {
13
13
  readonly name: "bun" | "node";
14
14
  serve(handler: (req: Request) => Promise<Response>, opts: {
15
15
  port: number;
16
+ hostname?: string;
16
17
  earlyHints?: () => string[];
17
18
  }): ServeHandle;
18
19
  spawnModule(entry: string, args: string[], opts: {
package/dist/host.js CHANGED
@@ -9,6 +9,7 @@ function bunHost() {
9
9
  serve(handler, opts) {
10
10
  const server = Bun.serve({
11
11
  port: opts.port,
12
+ hostname: opts.hostname,
12
13
  fetch: handler,
13
14
  idleTimeout: 0
14
15
  });
@@ -64,7 +65,7 @@ function nodeHost() {
64
65
  res.end("Internal Server Error");
65
66
  }
66
67
  });
67
- server.listen(opts.port);
68
+ server.listen(opts.port, opts.hostname);
68
69
  return {
69
70
  get port() {
70
71
  const addr = server.address();
package/dist/index.d.ts CHANGED
@@ -2,12 +2,12 @@ import { a as ResourcePlan, c as staticSite, i as JuneAdapter, l as vercel, n as
2
2
  import { AGENT_MODULE_FILE, AgentDirScan, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir } from "./agent-compile.js";
3
3
  import { CreateAppOptions, JuneApp, createApp } from "./app.js";
4
4
  import { R2Bucket, localBlob, r2 } from "./blob.js";
5
- import { r as scanRoutes } from "./route-scan-DMlwoESy.js";
5
+ import { o as scanRoutes } from "./route-scan-BMbBzPIB.js";
6
6
  import { t as generateContent } from "./content-freeze-CyiW2fRK.js";
7
7
  import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-BHUjBO0G.js";
8
- import { n as WorkerManifest, r as createWorker } from "./worker-Dv4l6Ybs.js";
9
- import { t as freezeConfig } from "./config-freeze-ZWC_Qd4h.js";
10
- import { n as buildManifest } from "./manifest-DSNX4I-G.js";
8
+ import { n as WorkerManifest, r as createWorker } from "./worker-2qbQjj2c.js";
9
+ import { t as freezeConfig } from "./config-freeze-CyXvNqi2.js";
10
+ import { n as buildManifest } from "./manifest-CwTT1zVs.js";
11
11
  import { BuildResult, juneBuild } from "./build.js";
12
12
  import { loadJuneConfig } from "./config-loader.js";
13
13
  import { AccountTokenStore, BetterAuthLike, ConnectionAuth, LinkedAccountAuthOptions, betterAuthAccessToken, betterAuthAccountTokenStore, linkedAccountAuth } from "./connection-auth.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { deno, staticSite, vercel, workers } from "./adapter.js";
2
2
  import { AGENT_MODULE_FILE, emitAgentModule, findAgentDir, generateAgentModule, scanAgentDir } from "./agent-compile.js";
3
3
  import { listRoutes, matchRoute, matchRouteTree, resolveNotFound } from "./router.js";
4
+ import { scanRoutes } from "./route-scan.js";
4
5
  import { negotiate } from "./negotiate.js";
5
6
  import { createPipeline } from "./pipeline.js";
6
7
  import { d1 } from "./d1.js";
@@ -12,7 +13,6 @@ import { freezeConfig } from "./config-freeze.js";
12
13
  import { collection, entry } from "./content.js";
13
14
  import { generateContent } from "./content-freeze.js";
14
15
  import { createWorker } from "./worker.js";
15
- import { scanRoutes } from "./route-scan.js";
16
16
  import { buildManifest } from "./manifest.js";
17
17
  import { juneBuild } from "./build.js";
18
18
  import { betterAuthAccessToken, betterAuthAccountTokenStore, linkedAccountAuth } from "./connection-auth.js";
@@ -1,5 +1,5 @@
1
1
  import { n as LayoutComponent } from "./pipeline-BHUjBO0G.js";
2
- import { n as WorkerManifest } from "./worker-Dv4l6Ybs.js";
2
+ import { n as WorkerManifest } from "./worker-2qbQjj2c.js";
3
3
  //#region src/manifest.d.ts
4
4
  type ImportedLayout = {
5
5
  component: LayoutComponent;
@@ -1,2 +1,2 @@
1
- import { n as buildManifest, r as importLayout, t as ImportedLayout } from "./manifest-DSNX4I-G.js";
1
+ import { n as buildManifest, r as importLayout, t as ImportedLayout } from "./manifest-CwTT1zVs.js";
2
2
  export { ImportedLayout, buildManifest, importLayout };
package/dist/manifest.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { findMiddlewareFile } from "./router.js";
2
+ import { scanAppRoutes } from "./route-scan.js";
2
3
  import { resolveBoundary } from "./segment.js";
3
4
  import { freezeConfig } from "./config-freeze.js";
4
- import { scanAppRoutes } from "./route-scan.js";
5
5
  import { existsSync } from "node:fs";
6
6
  import { basename, join } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
@@ -47,6 +47,7 @@ async function buildManifest(appRoot) {
47
47
  const routes = {};
48
48
  const dynamicRoutes = [];
49
49
  const resourceRoutes = [];
50
+ const generatedRoutes = scanned.filter((r) => r.generated).map((r) => r.path);
50
51
  const layoutChains = {};
51
52
  const layoutBoundaries = {};
52
53
  const loadings = {};
@@ -96,6 +97,7 @@ async function buildManifest(appRoot) {
96
97
  routes,
97
98
  dynamicRoutes,
98
99
  resourceRoutes,
100
+ ...generatedRoutes.length ? { generatedRoutes } : {},
99
101
  layoutChains,
100
102
  layoutBoundaries,
101
103
  loadings,
package/dist/pipeline.js CHANGED
@@ -9,8 +9,8 @@ import React, { Suspense, use } from "react";
9
9
  import { renderToReadableStream } from "react-dom/server";
10
10
  import { Document, PRERENDER_ORIGIN, documentTitle, pageCanonical } from "@junejs/core/document";
11
11
  import { AGENT_SKILLS_INDEX_PATH, AI_CATALOG_PATH, API_CATALOG_CONTENT_TYPE, ARD_PATH, MCP_SERVER_CARD_TYPE, agentSkillsIndex, aiCatalog, apiCatalog, buildLinkHeader, llmsTxt, mcpServerCard, robotsTxt, siteSkill, sitemapXml } from "@junejs/core/discovery";
12
- import { mcpHandler, mcpServerIdentity, mcpTools } from "@junejs/core/mcp";
13
- import { OPENAPI_MEDIA_TYPE, apiActionId, apiHandler, apiNamespaceResponse, isApiNamespace, openApiDocument } from "@junejs/core/api";
12
+ import { mcpForbidden, mcpHandler, mcpServerIdentity, mcpTools, originRejection } from "@junejs/core/mcp";
13
+ import { OPENAPI_MEDIA_TYPE, apiActionId, apiForbidden, apiHandler, apiNamespaceResponse, isApiNamespace, openApiDocument } from "@junejs/core/api";
14
14
  import { LOCALE_COOKIE, localeAlternates, localeDir, matchPinnedLocale, negotiateLocale } from "@junejs/core/i18n";
15
15
  //#region src/pipeline.ts
16
16
  function readCookie(request, name) {
@@ -368,18 +368,25 @@ function createPipeline(cfg) {
368
368
  default: return null;
369
369
  }
370
370
  }
371
+ const originPolicy = {
372
+ allowedOrigins: agent.allowedOrigins,
373
+ allowedHosts: agent.allowedHosts
374
+ };
375
+ const callerIdentity = async (request) => request.method === "POST" && cfg.identity ? await cfg.identity(request) : void 0;
371
376
  async function handleRequest(request) {
372
377
  const url = new URL(request.url);
373
378
  if (url.pathname === "/mcp") {
374
379
  if (!agent.mcp) return notFoundResponse("view", url.pathname);
375
- const identity = cfg.identity ? await cfg.identity(request) : void 0;
380
+ const refused = originRejection(request, originPolicy);
381
+ if (refused !== void 0) return mcpForbidden(refused);
382
+ const identity = await callerIdentity(request);
376
383
  return mcpHandler(request, {
377
384
  request,
378
385
  ...identity
379
386
  }, mcpServerIdentity(url.origin, {
380
387
  site: docConfig.site,
381
388
  agent
382
- }));
389
+ }), originPolicy);
383
390
  }
384
391
  if (agent.api) {
385
392
  if (url.pathname === "/openapi.json" && (request.method === "GET" || request.method === "HEAD")) {
@@ -394,11 +401,13 @@ function createPipeline(cfg) {
394
401
  }
395
402
  const actionId = apiActionId(url.pathname);
396
403
  if (actionId) {
397
- const identity = cfg.identity ? await cfg.identity(request) : void 0;
404
+ const refused = originRejection(request, originPolicy);
405
+ if (refused !== void 0) return apiForbidden(request, refused);
406
+ const identity = await callerIdentity(request);
398
407
  return apiHandler(request, actionId, {
399
408
  request,
400
409
  ...identity
401
- });
410
+ }, originPolicy);
402
411
  }
403
412
  }
404
413
  if (request.method === "OPTIONS" && agent.discovery && agent.mcp && url.pathname === SERVER_CARD_PATH) return new Response(null, {
@@ -0,0 +1,10 @@
1
+ //#region src/route-rank.d.ts
2
+ export type SegmentKind = "static" | "param" | "optional" | "catchAll" | "optionalCatchAll";
3
+ export declare function parseSegment(segment: string): {
4
+ kind: SegmentKind;
5
+ name?: string;
6
+ };
7
+ export declare function segmentRank(segment: string): number;
8
+ export declare function compareSegments(a: string, b: string): number;
9
+ export declare function compareRoutePatterns(a: string, b: string): number;
10
+ //#endregion
@@ -0,0 +1,38 @@
1
+ //#region src/route-rank.ts
2
+ const DYNAMIC = /^\[(\[)?(\.\.\.)?([A-Za-z_][A-Za-z0-9_]*)\](\])?$/;
3
+ function parseSegment(segment) {
4
+ const m = segment.match(DYNAMIC);
5
+ if (!m || !!m[1] !== !!m[4]) return { kind: "static" };
6
+ const optional = !!m[1];
7
+ return {
8
+ kind: !!m[2] ? optional ? "optionalCatchAll" : "catchAll" : optional ? "optional" : "param",
9
+ name: m[3]
10
+ };
11
+ }
12
+ const RANK = {
13
+ static: 0,
14
+ param: 1,
15
+ optional: 2,
16
+ catchAll: 3,
17
+ optionalCatchAll: 4
18
+ };
19
+ function segmentRank(segment) {
20
+ return RANK[parseSegment(segment).kind];
21
+ }
22
+ function compareSegments(a, b) {
23
+ return segmentRank(a) - segmentRank(b) || (a < b ? -1 : a > b ? 1 : 0);
24
+ }
25
+ function compareRoutePatterns(a, b) {
26
+ const as = a.split("/").filter(Boolean);
27
+ const bs = b.split("/").filter(Boolean);
28
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
29
+ const x = as[i];
30
+ const y = bs[i];
31
+ if (x === void 0) return -1;
32
+ if (y === void 0) return 1;
33
+ if (x !== y) return compareSegments(x, y);
34
+ }
35
+ return 0;
36
+ }
37
+ //#endregion
38
+ export { compareRoutePatterns, compareSegments, parseSegment, segmentRank };
@@ -0,0 +1,20 @@
1
+ //#region src/route-scan.d.ts
2
+ type RouteEntry = {
3
+ path: string;
4
+ file: string;
5
+ dynamic: boolean;
6
+ resource?: boolean;
7
+ generated?: boolean;
8
+ layouts: string[];
9
+ loading?: string;
10
+ };
11
+ declare function scanRoutes(appDir: string, dir?: string, layouts?: string[], out?: RouteEntry[], loading?: string): Promise<RouteEntry[]>;
12
+ declare function scanAppRoutes(appRoot: string): Promise<RouteEntry[]>;
13
+ declare function routeConflicts(routes: RouteEntry[]): Array<{
14
+ path: string;
15
+ files: string[];
16
+ }>;
17
+ declare function misplacedSegments(routes: RouteEntry[]): RouteEntry[];
18
+ declare function routeProblems(trees: RouteEntry[][], appRoot: string): string | null;
19
+ //#endregion
20
+ export { scanAppRoutes as a, routeProblems as i, misplacedSegments as n, scanRoutes as o, routeConflicts as r, RouteEntry as t };
@@ -1,2 +1,2 @@
1
- import { n as scanAppRoutes, r as scanRoutes, t as RouteEntry } from "./route-scan-DMlwoESy.js";
2
- export { RouteEntry, scanAppRoutes, scanRoutes };
1
+ import { a as scanAppRoutes, i as routeProblems, n as misplacedSegments, o as scanRoutes, r as routeConflicts, t as RouteEntry } from "./route-scan-BMbBzPIB.js";
2
+ export { RouteEntry, misplacedSegments, routeConflicts, routeProblems, scanAppRoutes, scanRoutes };
@@ -1,3 +1,4 @@
1
+ import { parseSegment } from "./route-rank.js";
1
2
  import { existsSync } from "node:fs";
2
3
  import { readdir } from "node:fs/promises";
3
4
  import { basename, join, relative, sep } from "node:path";
@@ -48,8 +49,52 @@ async function scanAppRoutes(appRoot) {
48
49
  const juneRoutesDir = join(appRoot, ".june", "routes");
49
50
  const appRoutes = await scanRoutes(appDir);
50
51
  const frameworkRoutes = existsSync(juneRoutesDir) ? await scanRoutes(juneRoutesDir) : [];
52
+ const problems = routeProblems([appRoutes, frameworkRoutes], appRoot);
53
+ if (problems) throw new Error(problems);
51
54
  const appPaths = new Set(appRoutes.map((r) => r.path));
52
- return [...appRoutes, ...frameworkRoutes.filter((r) => !appPaths.has(r.path))].sort((a, b) => a.path.localeCompare(b.path));
55
+ const generated = frameworkRoutes.filter((r) => !appPaths.has(r.path)).map((r) => ({
56
+ ...r,
57
+ generated: true
58
+ }));
59
+ return [...appRoutes, ...generated].sort((a, b) => a.path.localeCompare(b.path));
60
+ }
61
+ function routeConflicts(routes) {
62
+ const byKey = /* @__PURE__ */ new Map();
63
+ for (const r of routes) {
64
+ const key = `${r.resource ? "route" : "page"} ${r.path}`;
65
+ const hit = byKey.get(key);
66
+ if (hit) hit.files.push(r.file);
67
+ else byKey.set(key, {
68
+ path: r.path,
69
+ files: [r.file]
70
+ });
71
+ }
72
+ return [...byKey.values()].filter((c) => c.files.length > 1).map((c) => ({
73
+ path: c.path,
74
+ files: c.files.sort()
75
+ })).sort((a, b) => a.path.localeCompare(b.path));
76
+ }
77
+ const ENDS_PATH = /* @__PURE__ */ new Set([
78
+ "optional",
79
+ "catchAll",
80
+ "optionalCatchAll"
81
+ ]);
82
+ const beforeLast = (path) => path.split("/").filter(Boolean).slice(0, -1).map((s) => parseSegment(s).kind);
83
+ function misplacedSegments(routes) {
84
+ return routes.filter((r) => beforeLast(r.path).some((k) => ENDS_PATH.has(k))).sort((a, b) => a.path.localeCompare(b.path) || a.file.localeCompare(b.file));
85
+ }
86
+ function routeProblems(trees, appRoot) {
87
+ const rel = (f) => relative(appRoot, f).split(sep).join("/");
88
+ const sections = [];
89
+ const conflicts = trees.flatMap(routeConflicts);
90
+ if (conflicts.length) sections.push("[june] more than one route file resolves to the same path — keep one per path:\n" + conflicts.map((c) => ` ${c.path}: ${c.files.map(rel).join(", ")}`).join("\n"));
91
+ const misplaced = trees.flatMap(misplacedSegments);
92
+ if (misplaced.length) {
93
+ const lines = misplaced.map((r) => ` ${r.path}: ${rel(r.file)}`);
94
+ if (misplaced.some((r) => beforeLast(r.path).includes("optional"))) lines.push(" 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.");
95
+ sections.push("[june] an optional or catch-all segment must be the last segment of a route path — nothing may follow it:\n" + lines.join("\n"));
96
+ }
97
+ return sections.length ? sections.join("\n") : null;
53
98
  }
54
99
  //#endregion
55
- export { scanAppRoutes, scanRoutes };
100
+ export { misplacedSegments, routeConflicts, routeProblems, scanAppRoutes, scanRoutes };