@junejs/server 0.1.0-dev.5 → 0.1.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/dist/adapter.d.ts CHANGED
@@ -23,6 +23,7 @@ type AdapterEmitContext = {
23
23
  config: JuneConfig;
24
24
  plan: ResourcePlan;
25
25
  defaultName: string;
26
+ publicFiles?: string[];
26
27
  };
27
28
  interface JuneAdapter {
28
29
  readonly name: string;
package/dist/adapter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { copyFile, cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  //#region src/adapter.ts
5
5
  function workers(opts) {
6
6
  return {
@@ -78,7 +78,7 @@ function vercel(opts) {
78
78
  wrap: (pipelineVar) => isEdge ? env + `export default (request) => ${pipelineVar}.fetch(request, __env);` : env + `export default { fetch: (request) => ${pipelineVar}.fetch(request, __env) };`
79
79
  };
80
80
  },
81
- async emit({ appRoot, outDir, hasAssets }) {
81
+ async emit({ appRoot, outDir, hasAssets, publicFiles }) {
82
82
  const out = join(appRoot, ".vercel", "output");
83
83
  const fnDir = join(out, "functions", `${VERCEL_FUNCTION}.func`);
84
84
  await rm(out, {
@@ -106,6 +106,13 @@ function vercel(opts) {
106
106
  const src = join(outDir, "assets", "_june");
107
107
  if (existsSync(src)) await cp(src, join(out, "static", "_june"), { recursive: true });
108
108
  }
109
+ for (const rel of publicFiles ?? []) {
110
+ const from = join(outDir, "assets", rel);
111
+ if (!existsSync(from)) continue;
112
+ const to = join(out, "static", rel);
113
+ await mkdir(dirname(to), { recursive: true });
114
+ await copyFile(from, to);
115
+ }
109
116
  await writeFile(join(out, "config.json"), JSON.stringify({
110
117
  version: 3,
111
118
  routes: [
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { basename, extname, join } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { defineAgent } from "@junejs/core/agent-config";
5
+ import { defineAgent, resolveChannel } from "@junejs/core/agent-config";
6
6
  import { connectAll } from "@junejs/core/connections";
7
7
  //#region src/agent-discover.ts
8
8
  async function scan(dir, ext) {
@@ -48,7 +48,7 @@ async function discoverAgent(dir) {
48
48
  const channels = [];
49
49
  for (const f of await scan(join(dir, "channels"), ".ts")) {
50
50
  const mod = await import(pathToFileURL(f).href);
51
- if (mod.default) channels.push(mod.default);
51
+ if (mod.default) channels.push(resolveChannel(mod.default, process.env));
52
52
  }
53
53
  const connectionDefs = [];
54
54
  for (const f of await scan(join(dir, "connections"), ".ts")) {
@@ -1,4 +1,6 @@
1
+ import { Channel, ChannelFactory } from "@junejs/core/agent-config";
1
2
  import { Model, Msg, SessionStore, Tool } from "@junejs/core/agent-runtime";
3
+ import { Resources } from "@junejs/core/resources";
2
4
  //#region src/agent-durable.d.ts
3
5
  interface SqlStorageCursor<T = Record<string, unknown>> {
4
6
  toArray(): T[];
@@ -40,9 +42,13 @@ type DoAgentDef = {
40
42
  model: Model;
41
43
  tools: Tool[];
42
44
  instructions?: string;
45
+ resources?: Resources;
46
+ services?: unknown;
43
47
  };
44
48
  declare class AgentDurableObject {
45
49
  private session;
50
+ private readonly resources;
51
+ private readonly services;
46
52
  constructor(state: DurableObjectState, def: DoAgentDef);
47
53
  turn(input: {
48
54
  turnId?: string;
@@ -56,5 +62,11 @@ declare function durableAgentSurface(getNamespace: () => DurableObjectNamespace
56
62
  agentName: string;
57
63
  chatPath?: string;
58
64
  }): (req: Request) => Promise<Response | null>;
65
+ declare function durableChannelSurface(getNamespace: () => DurableObjectNamespace | undefined, opts: {
66
+ agentName: string;
67
+ channels: (Channel | ChannelFactory)[];
68
+ env: unknown;
69
+ waitUntil?: (p: Promise<unknown>) => void;
70
+ }): (req: Request) => Promise<Response | null>;
59
71
  //#endregion
60
- export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, durableAgentSurface, durableFetch };
72
+ export { AgentDurableObject, DoAgentDef, DoSessionStore, DurableObjectNamespace, DurableObjectState, DurableObjectStub, DurableStorage, SqlStorage, SqlStorageCursor, durableAgentSurface, durableChannelSurface, durableFetch };
@@ -1,4 +1,6 @@
1
+ import { channelDispatch, resolveChannel } from "@junejs/core/agent-config";
1
2
  import { AgentSession, withSystem } from "@junejs/core/agent-runtime";
3
+ import { ensureScope, runInScope } from "@junejs/db";
2
4
  //#region src/agent-durable.ts
3
5
  var DoSessionStore = class {
4
6
  storage;
@@ -61,13 +63,21 @@ const crossDoUnsupported = { session() {
61
63
  } };
62
64
  var AgentDurableObject = class {
63
65
  session;
66
+ resources;
67
+ services;
64
68
  constructor(state, def) {
65
69
  const store = new DoSessionStore(state.storage);
66
70
  const model = def.instructions ? withSystem(def.model, def.instructions) : def.model;
71
+ this.resources = def.resources ?? {};
72
+ this.services = def.services;
67
73
  this.session = new AgentSession(def.name ?? "agent", "self", store, new InProcBroadcaster(), model, def.tools, crossDoUnsupported);
68
74
  }
69
- turn(input) {
70
- return this.session.turn(input);
75
+ async turn(input) {
76
+ await ensureScope();
77
+ return runInScope({
78
+ resources: this.resources,
79
+ services: this.services
80
+ }, () => this.session.turn(input));
71
81
  }
72
82
  transcript() {
73
83
  return this.session.transcript();
@@ -102,5 +112,30 @@ function durableAgentSurface(getNamespace, opts) {
102
112
  }));
103
113
  };
104
114
  }
115
+ function durableChannelSurface(getNamespace, opts) {
116
+ return channelDispatch(opts.channels.map((c) => resolveChannel(c, opts.env)), {
117
+ agent: {
118
+ name: opts.agentName,
119
+ instructions: "",
120
+ tools: [],
121
+ skills: [],
122
+ channels: [],
123
+ connections: []
124
+ },
125
+ waitUntil: opts.waitUntil,
126
+ run: async (message, o) => {
127
+ const namespace = getNamespace();
128
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
129
+ const { text } = await (await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn", {
130
+ method: "POST",
131
+ body: JSON.stringify({
132
+ userText: message,
133
+ turnId: o?.turnId
134
+ })
135
+ }))).json();
136
+ return text;
137
+ }
138
+ });
139
+ }
105
140
  //#endregion
106
- export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableFetch };
141
+ export { AgentDurableObject, DoSessionStore, durableAgentSurface, durableChannelSurface, durableFetch };
package/dist/app.js CHANGED
@@ -8,7 +8,9 @@ import { MODULE_STYLES_URL, buildModuleCss, registerCssModules } from "./css-mod
8
8
  import { CLIENT_SCRIPT_URL, bundleClientSplit, findClientEntry } from "./client-bundle.js";
9
9
  import { generateIslandRegistry } from "./island-registry.js";
10
10
  import { STYLES_URL, findGlobalCss, globalCssUsesTailwind, processCssCached } from "./css.js";
11
- import { existsSync } from "node:fs";
11
+ import { contentTypeFor, safeRelativePath } from "./static-files.js";
12
+ import { existsSync, lstatSync, realpathSync } from "node:fs";
13
+ import { readFile } from "node:fs/promises";
12
14
  import { dirname, join, resolve } from "node:path";
13
15
  import { pathToFileURL } from "node:url";
14
16
  import { buildSystemPrompt } from "@junejs/core/agent-config";
@@ -62,6 +64,11 @@ function createApp({ appDir: appDirInput, config = {} }) {
62
64
  const appDir = resolve(appDirInput);
63
65
  const juneRoutesDir = join(dirname(appDir), ".june", "routes");
64
66
  const hasJuneRoutes = existsSync(juneRoutesDir);
67
+ const publicDir = join(dirname(appDir), "public");
68
+ let publicRoot = null;
69
+ try {
70
+ if (lstatSync(publicDir).isDirectory()) publicRoot = realpathSync(publicDir);
71
+ } catch {}
65
72
  const agent = resolveAgent(config.agent);
66
73
  const speculation = config.speculation;
67
74
  const clientEntry = findClientEntry(appDir) ?? findClientEntry(join(dirname(appDir), ".june", "routes"));
@@ -91,6 +98,8 @@ function createApp({ appDir: appDirInput, config = {} }) {
91
98
  return [...own, ...gen].sort();
92
99
  };
93
100
  const resources = memoizeResources(config.resources);
101
+ let servicesCache;
102
+ const services = config.services ? () => (servicesCache ??= { v: config.services.make(process.env) }).v : void 0;
94
103
  config.dataLayer?.install();
95
104
  let pipelinePromise;
96
105
  const getPipeline = () => pipelinePromise ??= buildPipeline();
@@ -136,6 +145,7 @@ function createApp({ appDir: appDirInput, config = {} }) {
136
145
  routeList: routePaths,
137
146
  earlyHints: config.earlyHints,
138
147
  resources,
148
+ services,
139
149
  notFoundComponent,
140
150
  resolve: async (pathname) => {
141
151
  const match = await matchRouteTree(appDir, pathname, { pageConvention: true }) ?? (hasJuneRoutes ? await matchRouteTree(juneRoutesDir, pathname, { pageConvention: true }) : null);
@@ -201,7 +211,34 @@ function createApp({ appDir: appDirInput, config = {} }) {
201
211
  });
202
212
  }
203
213
  }
204
- return runWithTrace(newTrace(), async () => (await getPipeline()).fetch(request));
214
+ const toPipeline = () => runWithTrace(newTrace(), async () => (await getPipeline()).fetch(request));
215
+ if (publicRoot !== null && (request.method === "GET" || request.method === "HEAD")) {
216
+ const rel = safeRelativePath(new URL(request.url).pathname);
217
+ if (rel !== null && rel.split("/")[0] !== "_june") {
218
+ const segments = rel.split("/");
219
+ const file = join(publicRoot, ...segments);
220
+ let stat;
221
+ try {
222
+ stat = lstatSync(file);
223
+ } catch {}
224
+ let real = null;
225
+ if (stat?.isFile()) try {
226
+ real = realpathSync(file);
227
+ } catch {}
228
+ if (real === file) {
229
+ const headers = {
230
+ "content-type": contentTypeFor(rel),
231
+ "cache-control": "public, max-age=0, must-revalidate"
232
+ };
233
+ if (request.method === "HEAD") return Promise.resolve(new Response(null, { headers: {
234
+ ...headers,
235
+ "content-length": String(stat.size)
236
+ } }));
237
+ return readFile(file).then((buf) => new Response(buf, { headers }), () => toPipeline());
238
+ }
239
+ }
240
+ }
241
+ return toPipeline();
205
242
  },
206
243
  async warmup() {
207
244
  const files = await routeFiles(appDir, { pageConvention: true });
package/dist/build.js CHANGED
@@ -6,12 +6,13 @@ import { jsxTransform } from "./tsconfig-jsx.js";
6
6
  import { CLIENT_SCRIPT_URL, bundleClientToFile, findClientEntry } from "./client-bundle.js";
7
7
  import { generateIslandRegistry } from "./island-registry.js";
8
8
  import { STYLES_URL, cssTargets, findGlobalCss, globalCssUsesTailwind, minifyCss, processCss } from "./css.js";
9
+ import { RESERVED_PREFIX } from "./static-files.js";
9
10
  import { findJuneConfigPath, loadJuneConfig } from "./config-loader.js";
10
11
  import { generateContentModule } from "./content.js";
11
12
  import { createWorker } from "./worker.js";
12
13
  import { buildRsc, findRscRoutes } from "./rsc-build.js";
13
- import { existsSync } from "node:fs";
14
- import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
14
+ import { existsSync, lstatSync } from "node:fs";
15
+ import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
15
16
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
16
17
  import { pathToFileURL } from "node:url";
17
18
  import { routeFromModule } from "@junejs/core/route";
@@ -279,6 +280,15 @@ function importPath(fromDir, file) {
279
280
  const p = relative(fromDir, file).split(sep).join("/").replace(/\.[^.]+$/, "");
280
281
  return p.startsWith(".") ? p : `./${p}`;
281
282
  }
283
+ async function collectFiles(dir, base = dir) {
284
+ const out = [];
285
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
286
+ const abs = join(dir, entry.name);
287
+ if (entry.isDirectory()) out.push(...await collectFiles(abs, base));
288
+ else if (entry.isFile()) out.push(relative(base, abs).split(sep).join("/"));
289
+ }
290
+ return out;
291
+ }
282
292
  async function juneBuild(appRoot, options = {}) {
283
293
  const appDir = join(appRoot, "app");
284
294
  if (!existsSync(appDir)) throw new Error(`no app/ directory in ${appRoot} — is this a June app?`);
@@ -401,6 +411,12 @@ async function juneBuild(appRoot, options = {}) {
401
411
  const dataLayerModule = fullConfig.dataLayer?.module;
402
412
  if (dataLayerModule) imports.push(`import { installDataLayer } from ${JSON.stringify(dataLayerModule)};`);
403
413
  const dataLayerBoot = dataLayerModule ? "\ninstallDataLayer();\n" : "";
414
+ const servicesModule = fullConfig.services?.module;
415
+ if (servicesModule) {
416
+ const spec = /^[./]/.test(servicesModule) ? importPath(genDir, resolve(appRoot, servicesModule)) : servicesModule;
417
+ imports.push(`import { services as __appServices } from ${JSON.stringify(spec)};`);
418
+ }
419
+ const servicesField = servicesModule ? `\n services: __appServices,` : "";
404
420
  const entry = `// AUTO-GENERATED by \`june build\` — do not edit. Regenerate: june build .
405
421
  ${adapterEntry.imports.join("\n")}
406
422
  ${imports.join("\n")}
@@ -420,7 +436,7 @@ ${loadings.join("\n")}
420
436
  },
421
437
  document: ${JSON.stringify(frozen.document, null, 2).replace(/\n/g, "\n ")},
422
438
  agent: ${JSON.stringify(frozen.agent)},${frozen.i18n ? `\n i18n: ${JSON.stringify(frozen.i18n)},` : ""}
423
- earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}
439
+ earlyHints: ${JSON.stringify(frozen.earlyHints)},${builtExtraFile ? "\n extra," : ""}${resourcesField}${servicesField}
424
440
  });
425
441
 
426
442
  ${adapterEntry.wrap("pipeline")}
@@ -459,6 +475,26 @@ ${adapterEntry.wrap("pipeline")}
459
475
  if (moduleCssAsset) manifest.document.moduleStyles = `/${moduleCssAsset}`;
460
476
  const worker = createWorker(manifest);
461
477
  let hasAssets = false;
478
+ const publicDir = join(appRoot, "public");
479
+ const publicFiles = [];
480
+ let publicIsDir = false;
481
+ try {
482
+ publicIsDir = lstatSync(publicDir).isDirectory();
483
+ } catch {}
484
+ if (existsSync(publicDir) && !publicIsDir) console.warn(`[june] public/ is not a real directory (symlink?) — skipped`);
485
+ if (publicIsDir) {
486
+ for (const rel of await collectFiles(publicDir)) {
487
+ if (rel.split("/")[0] === "_june") {
488
+ console.warn(`[june] public/${rel} ignored — ${RESERVED_PREFIX}/ is reserved for framework assets`);
489
+ continue;
490
+ }
491
+ const dest = join(assetsDir, rel);
492
+ await mkdir(dirname(dest), { recursive: true });
493
+ await copyFile(join(publicDir, ...rel.split("/")), dest);
494
+ publicFiles.push(rel);
495
+ }
496
+ if (publicFiles.length) hasAssets = true;
497
+ }
462
498
  const isStatic = adapter.capabilities.runtime === "static";
463
499
  const i18n = frozen.i18n;
464
500
  const prerenderOne = async (reqPath, def) => {
@@ -536,7 +572,8 @@ ${adapterEntry.wrap("pipeline")}
536
572
  linkHeader,
537
573
  config: fullConfig,
538
574
  plan,
539
- defaultName
575
+ defaultName,
576
+ publicFiles
540
577
  });
541
578
  if (plan.db) console.log(` ↳ d1 binding "${plan.db.binding}" emitted — set database_id in wrangler.jsonc (wrangler d1 create ${plan.db.databaseName})`);
542
579
  return {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AdapterCapabilities, AdapterEmitContext, AdapterEntry, JuneAdapter, ResourcePlan, deno, staticSite, vercel, workers } from "./adapter.js";
2
2
  import { CreateAppOptions, JuneApp, createApp } from "./app.js";
3
3
  import { R2Bucket, localBlob, r2 } from "./blob.js";
4
- import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-B2V6tKfO.js";
4
+ import { a as Pipeline, d as createPipeline, n as LayoutComponent, o as PipelineConfig, s as Resolved, u as RouteResolver } from "./pipeline-CCp9idQ4.js";
5
5
  import { WorkerManifest, createWorker } from "./worker.js";
6
6
  import { BuildResult, buildManifest, freezeConfig, generateContent, juneBuild, scanRoutes } from "./build.js";
7
7
  import { loadJuneConfig } from "./config-loader.js";
@@ -21,6 +21,7 @@ import { ResourceFlags, WorkerEnv, bindWorkerResources, memoizeResources } from
21
21
  import { MatchOptions, RouteMatch, RouteTreeMatch, SegmentMatch, listRoutes, matchRoute, matchRouteTree, resolveNotFound } from "./router.js";
22
22
  import { installAsyncContext } from "./instrumentation.js";
23
23
  import { Negotiated, negotiate } from "./negotiate.js";
24
- import { blob, db, kv } from "@junejs/db";
24
+ import { blob, currentServices, db, kv } from "@junejs/db";
25
+ import { ServicesConfig, defineServices } from "@junejs/core/config";
25
26
  import { I18nConfig, LOCALE_COOKIE, LocaleConfig, LocaleMatch, dirForLang, localeDir, localeHref, matchPinnedLocale, negotiateLocale, parseAcceptLanguage, resolveRequestLocale } from "@junejs/core/i18n";
26
- export { type AdapterCapabilities, type AdapterEmitContext, type AdapterEntry, type BuildResult, type ContentEntry, type CreateAppOptions, type D1Database, type D1Exec, type DeployResult, type DevServer, type DevServerOptions, type I18nConfig, type JuneAdapter, type JuneApp, type JuneDb, type JuneHost, LOCALE_COOKIE, type LayoutComponent, type LocaleConfig, type LocaleMatch, type MatchOptions, type MigrateResult, type Migration, type Negotiated, type Pipeline, type PipelineConfig, type R2Bucket, type Resolved, type ResourceFlags, type ResourcePlan, type RouteMatch, type RouteResolver, type RouteTreeMatch, type RunResult, type SegmentMatch, type ServeHandle, type SpawnedModule, type WorkerEnv, type WorkerManifest, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, d1, db, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
27
+ export { type AdapterCapabilities, type AdapterEmitContext, type AdapterEntry, type BuildResult, type ContentEntry, type CreateAppOptions, type D1Database, type D1Exec, type DeployResult, type DevServer, type DevServerOptions, type I18nConfig, type JuneAdapter, type JuneApp, type JuneDb, type JuneHost, LOCALE_COOKIE, type LayoutComponent, type LocaleConfig, type LocaleMatch, type MatchOptions, type MigrateResult, type Migration, type Negotiated, type Pipeline, type PipelineConfig, type R2Bucket, type Resolved, type ResourceFlags, type ResourcePlan, type RouteMatch, type RouteResolver, type RouteTreeMatch, type RunResult, type SegmentMatch, type ServeHandle, type ServicesConfig, type SpawnedModule, type WorkerEnv, type WorkerManifest, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { installAsyncContext } from "./instrumentation.js";
21
21
  import { host } from "./host.js";
22
22
  import { startDevServer } from "./dev.js";
23
23
  import { memoryKv, redisKv } from "./kv.js";
24
- import { blob, db, kv } from "@junejs/db";
24
+ import { blob, currentServices, db, kv } from "@junejs/db";
25
+ import { defineServices } from "@junejs/core/config";
25
26
  import { LOCALE_COOKIE, dirForLang, localeDir, localeHref, matchPinnedLocale, negotiateLocale, parseAcceptLanguage, resolveRequestLocale } from "@junejs/core/i18n";
26
- export { LOCALE_COOKIE, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, d1, db, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
27
+ export { LOCALE_COOKIE, bindWorkerResources, blob, blockedMessage, buildManifest, classify, collection, createApp, createPipeline, createWorker, currentServices, d1, db, defineServices, deno, dirForLang, entry, freezeConfig, generateContent, host, installAsyncContext, juneBuild, juneDeploy, kv, listRoutes, loadJuneConfig, localBlob, localeDir, localeHref, matchPinnedLocale, matchRoute, matchRouteTree, memoizeResources, memoryKv, migrate, migrateApp, migrateD1, mysql, negotiate, negotiateLocale, parseAcceptLanguage, postgres, r2, readMigrations, redisKv, resolveD1Database, resolveNotFound, resolveRequestLocale, scanRoutes, sqlite, startDevServer, staticSite, turso, typesApp, vercel, workers, wranglerD1 };
@@ -31,6 +31,7 @@ type PipelineConfig = {
31
31
  routeList: () => Promise<string[]> | string[];
32
32
  resolve: RouteResolver;
33
33
  resources?: () => Promise<Resources> | Resources;
34
+ services?: () => Promise<unknown> | unknown;
34
35
  earlyHints?: string[];
35
36
  htmlCacheControl?: string;
36
37
  notFoundComponent?: React.ComponentType<{
@@ -1,2 +1,2 @@
1
- import { a as Pipeline, c as ResolvedResource, d as createPipeline, i as MiddlewareHandler, l as ResourceHandler, n as LayoutComponent, o as PipelineConfig, r as LoadingComponent, s as Resolved, t as ExtraHandler, u as RouteResolver } from "./pipeline-B2V6tKfO.js";
1
+ import { a as Pipeline, c as ResolvedResource, d as createPipeline, i as MiddlewareHandler, l as ResourceHandler, n as LayoutComponent, o as PipelineConfig, r as LoadingComponent, s as Resolved, t as ExtraHandler, u as RouteResolver } from "./pipeline-CCp9idQ4.js";
2
2
  export { ExtraHandler, LayoutComponent, LoadingComponent, MiddlewareHandler, Pipeline, PipelineConfig, Resolved, ResolvedResource, ResourceHandler, RouteResolver, createPipeline };
package/dist/pipeline.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { SEGMENT_HEADER, TITLE_HEADER, encodeTitle, negotiate } from "./negotiate.js";
2
+ import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
2
3
  import { LoaderDataContext } from "@junejs/core/route";
3
4
  import React, { Suspense, use } from "react";
4
5
  import { renderToReadableStream } from "react-dom/server";
5
6
  import { Document } from "@junejs/core/document";
6
7
  import { apiCatalog, buildLinkHeader, llmsTxt, mcpServerCard, robotsTxt, sitemapXml } from "@junejs/core/discovery";
7
8
  import { mcpHandler, mcpTools } from "@junejs/core/mcp";
8
- import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
9
9
  import { LOCALE_COOKIE, localeAlternates, localeDir, matchPinnedLocale, negotiateLocale } from "@junejs/core/i18n";
10
10
  //#region src/pipeline.ts
11
11
  function readCookie(request, name) {
@@ -228,7 +228,10 @@ function createPipeline(cfg) {
228
228
  }
229
229
  return { async fetch(request) {
230
230
  await ensureScope();
231
- return runInScope({ resources: cfg.resources ? await cfg.resources() : {} }, () => handleRequest(request));
231
+ return runInScope({
232
+ resources: cfg.resources ? await cfg.resources() : {},
233
+ services: cfg.services ? await cfg.services() : void 0
234
+ }, () => handleRequest(request));
232
235
  } };
233
236
  }
234
237
  //#endregion
@@ -0,0 +1,6 @@
1
+ //#region src/static-files.d.ts
2
+ declare const RESERVED_PREFIX = "_june";
3
+ declare function contentTypeFor(pathname: string): string;
4
+ declare function safeRelativePath(pathname: string): string | null;
5
+ //#endregion
6
+ export { RESERVED_PREFIX, contentTypeFor, safeRelativePath };
@@ -0,0 +1,56 @@
1
+ //#region src/static-files.ts
2
+ const RESERVED_PREFIX = "_june";
3
+ const CONTENT_TYPES = {
4
+ html: "text/html; charset=utf-8",
5
+ css: "text/css; charset=utf-8",
6
+ js: "text/javascript; charset=utf-8",
7
+ mjs: "text/javascript; charset=utf-8",
8
+ json: "application/json; charset=utf-8",
9
+ map: "application/json; charset=utf-8",
10
+ xml: "application/xml; charset=utf-8",
11
+ txt: "text/plain; charset=utf-8",
12
+ md: "text/markdown; charset=utf-8",
13
+ csv: "text/csv; charset=utf-8",
14
+ webmanifest: "application/manifest+json",
15
+ svg: "image/svg+xml",
16
+ png: "image/png",
17
+ jpg: "image/jpeg",
18
+ jpeg: "image/jpeg",
19
+ gif: "image/gif",
20
+ webp: "image/webp",
21
+ avif: "image/avif",
22
+ ico: "image/x-icon",
23
+ bmp: "image/bmp",
24
+ woff2: "font/woff2",
25
+ woff: "font/woff",
26
+ ttf: "font/ttf",
27
+ otf: "font/otf",
28
+ mp4: "video/mp4",
29
+ webm: "video/webm",
30
+ mp3: "audio/mpeg",
31
+ ogg: "audio/ogg",
32
+ wav: "audio/wav",
33
+ wasm: "application/wasm",
34
+ pdf: "application/pdf",
35
+ zip: "application/zip"
36
+ };
37
+ function contentTypeFor(pathname) {
38
+ const dot = pathname.lastIndexOf(".");
39
+ if (dot === -1) return "application/octet-stream";
40
+ return CONTENT_TYPES[pathname.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
41
+ }
42
+ function safeRelativePath(pathname) {
43
+ let decoded;
44
+ try {
45
+ decoded = decodeURIComponent(pathname);
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (decoded.includes("\0") || decoded.includes("\\")) return null;
50
+ const segments = decoded.split("/").filter((s) => s !== "" && s !== ".");
51
+ if (segments.length === 0) return null;
52
+ if (segments.some((s) => s === "..")) return null;
53
+ return segments.join("/");
54
+ }
55
+ //#endregion
56
+ export { RESERVED_PREFIX, contentTypeFor, safeRelativePath };
package/dist/worker.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-B2V6tKfO.js";
1
+ import { l as ResourceHandler, n as LayoutComponent, r as LoadingComponent, t as ExtraHandler } from "./pipeline-CCp9idQ4.js";
2
2
  import { BrandedRoute } from "@junejs/core/route";
3
3
  import { AgentConfig } from "@junejs/core/config";
4
4
  import React from "react";
@@ -33,6 +33,7 @@ type WorkerManifest = {
33
33
  }>;
34
34
  extra?: ExtraHandler;
35
35
  resources?: (env?: unknown) => Promise<Resources> | Resources;
36
+ services?: (env?: unknown) => unknown;
36
37
  };
37
38
  declare function createWorker(manifest: WorkerManifest): {
38
39
  fetch(request: Request, env?: unknown): Promise<Response>;
package/dist/worker.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { durableAgentSurface } from "./agent-durable.js";
2
2
  import { createPipeline } from "./pipeline.js";
3
+ import { RESERVED_PREFIX, contentTypeFor, safeRelativePath } from "./static-files.js";
3
4
  //#region src/worker.ts
4
5
  function compilePattern(pattern) {
5
6
  const names = [];
@@ -61,6 +62,8 @@ function createWorker(manifest) {
61
62
  const boundaryFor = (key) => manifest.layoutBoundaries?.[key] ?? null;
62
63
  let currentEnv;
63
64
  const provider = manifest.resources;
65
+ const servicesProvider = manifest.services;
66
+ let servicesCache;
64
67
  const agentSurface = manifest.agent.runtime.enabled && manifest.agentName ? (() => {
65
68
  const route = durableAgentSurface(() => currentEnv?.AGENT, {
66
69
  agentName: manifest.agentName,
@@ -79,6 +82,7 @@ function createWorker(manifest) {
79
82
  notFoundComponent: manifest.notFound,
80
83
  extra: manifest.extra,
81
84
  resources: provider ? () => provider(currentEnv) : void 0,
85
+ services: servicesProvider ? () => (servicesCache ??= { v: servicesProvider(currentEnv) }).v : void 0,
82
86
  resolve: async (pathname) => {
83
87
  const staticDef = manifest.routes[pathname];
84
88
  if (staticDef) {
@@ -176,32 +180,33 @@ function withAssets(pipeline, opts = {}) {
176
180
  return pipeline.fetch(request, env);
177
181
  } };
178
182
  }
179
- const ASSET_CONTENT_TYPES = {
180
- css: "text/css; charset=utf-8",
181
- js: "text/javascript; charset=utf-8",
182
- json: "application/json; charset=utf-8",
183
- map: "application/json; charset=utf-8",
184
- svg: "image/svg+xml",
185
- png: "image/png",
186
- jpg: "image/jpeg",
187
- jpeg: "image/jpeg",
188
- webp: "image/webp",
189
- avif: "image/avif",
190
- ico: "image/x-icon",
191
- woff2: "font/woff2",
192
- woff: "font/woff"
193
- };
194
183
  function withDenoAssets(pipeline) {
195
184
  return async (request) => {
196
- const url = new URL(request.url);
197
- if (request.method === "GET" && url.pathname.startsWith("/_june/")) try {
198
- const file = await Deno.readFile(new URL(`./assets${url.pathname}`, import.meta.url));
199
- const ext = url.pathname.split(".").pop()?.toLowerCase() ?? "";
200
- return new Response(file, { headers: {
201
- "content-type": ASSET_CONTENT_TYPES[ext] ?? "application/octet-stream",
202
- "cache-control": "public, max-age=31536000, immutable"
203
- } });
204
- } catch {}
185
+ const method = request.method;
186
+ if (method === "GET" || method === "HEAD") {
187
+ const url = new URL(request.url);
188
+ {
189
+ const rel = safeRelativePath(url.pathname);
190
+ if (rel !== null) try {
191
+ const assetUrl = new URL(`./assets/${rel.split("/").map(encodeURIComponent).join("/")}`, import.meta.url);
192
+ const immutable = rel.startsWith(`${RESERVED_PREFIX}/`);
193
+ const headers = {
194
+ "content-type": contentTypeFor(rel),
195
+ "cache-control": immutable ? "public, max-age=31536000, immutable" : "public, max-age=0, must-revalidate"
196
+ };
197
+ if (method === "HEAD") {
198
+ const info = await Deno.stat(assetUrl);
199
+ if (!info.isFile) throw new Error("not a file");
200
+ return new Response(null, { headers: {
201
+ ...headers,
202
+ "content-length": String(info.size)
203
+ } });
204
+ }
205
+ const file = await Deno.readFile(assetUrl);
206
+ return new Response(file, { headers });
207
+ } catch {}
208
+ }
209
+ }
205
210
  return pipeline.fetch(request);
206
211
  };
207
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.1.0-dev.5",
3
+ "version": "0.1.0",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -125,8 +125,8 @@
125
125
  "build": "tsdown"
126
126
  },
127
127
  "dependencies": {
128
- "@junejs/core": ">=0.1.0-dev.4",
129
- "@junejs/db": ">=0.0.33-dev.1",
128
+ "@junejs/core": ">=0.1.0",
129
+ "@junejs/db": ">=0.0.33",
130
130
  "@momiji-rs/sparkdown": "^0.0.4",
131
131
  "lightningcss": "^1.27.0",
132
132
  "oxc-parser": "^0.137.0",
package/src/adapter.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // are explicit `deploy: { adapter: vercel() }` from their own package.
9
9
  import { existsSync } from "node:fs";
10
10
  import { cp, copyFile, mkdir, readdir, rm, writeFile } from "node:fs/promises";
11
- import { basename, join } from "node:path";
11
+ import { basename, dirname, join } from "node:path";
12
12
 
13
13
  import type { JuneConfig } from "@junejs/core/config";
14
14
 
@@ -49,6 +49,10 @@ export type AdapterEmitContext = {
49
49
  plan: ResourcePlan;
50
50
  // package.json name (or sanitized dir name) — the fallback worker/app name.
51
51
  defaultName: string;
52
+ // Verbatim public/ files (forward-slash paths relative to assets/), already
53
+ // copied into outDir/assets. Adapters that build a separate static tier (vercel)
54
+ // place these there; whole-assets targets (workers/static/deno) ignore it.
55
+ publicFiles?: string[];
52
56
  };
53
57
 
54
58
  export interface JuneAdapter {
@@ -214,7 +218,7 @@ export function vercel(opts?: { runtime?: "node" | "edge"; regions?: string[] })
214
218
  };
215
219
  },
216
220
 
217
- async emit({ appRoot, outDir, hasAssets }) {
221
+ async emit({ appRoot, outDir, hasAssets, publicFiles }) {
218
222
  const out = join(appRoot, ".vercel", "output");
219
223
  const fnDir = join(out, "functions", `${VERCEL_FUNCTION}.func`);
220
224
  await rm(out, { recursive: true, force: true }); // clean — no stale outputs
@@ -249,11 +253,20 @@ export function vercel(opts?: { runtime?: "node" | "edge"; regions?: string[] })
249
253
  await writeFile(join(fnDir, "package.json"), JSON.stringify({ type: "module" }, null, 2) + "\n");
250
254
  }
251
255
 
252
- // static: only the hashed framework assets under /_june/ (exact-path URLs)
256
+ // static: the hashed framework assets under /_june/ (exact-path URLs)...
253
257
  if (hasAssets) {
254
258
  const src = join(outDir, "assets", "_june");
255
259
  if (existsSync(src)) await cp(src, join(out, "static", "_june"), { recursive: true });
256
260
  }
261
+ // ...plus verbatim public/ files, served by the `filesystem` route handle
262
+ // below. Only these — prerendered page HTML stays on the SSR function.
263
+ for (const rel of publicFiles ?? []) {
264
+ const from = join(outDir, "assets", rel);
265
+ if (!existsSync(from)) continue;
266
+ const to = join(out, "static", rel);
267
+ await mkdir(dirname(to), { recursive: true });
268
+ await copyFile(from, to);
269
+ }
257
270
 
258
271
  await writeFile(
259
272
  join(out, "config.json"),
@@ -21,7 +21,7 @@ import { basename, extname, join } from "node:path";
21
21
  import { pathToFileURL } from "node:url";
22
22
 
23
23
  import type { AnyAction } from "@junejs/core/agent";
24
- import { defineAgent, type AgentConfigFile, type AgentDefinition, type Channel, type Skill } from "@junejs/core/agent-config";
24
+ import { defineAgent, resolveChannel, type AgentConfigFile, type AgentDefinition, type Channel, type ChannelFactory, type Skill } from "@junejs/core/agent-config";
25
25
  import { connectAll, type Connection } from "@junejs/core/connections";
26
26
 
27
27
  async function scan(dir: string, ext: string): Promise<string[]> {
@@ -79,10 +79,13 @@ export async function discoverAgent(dir: string): Promise<AgentDefinition> {
79
79
  skills.push(parseSkill(basename(f, ".md"), await readFile(f, "utf8")));
80
80
  }
81
81
 
82
+ // A channel module default-exports a Channel OR a `(env) => Channel` factory (the
83
+ // form workerd needs — secrets live in env, not at module scope). On native, env is
84
+ // process.env (present at load), so resolve here; the plain form stays supported.
82
85
  const channels: Channel[] = [];
83
86
  for (const f of await scan(join(dir, "channels"), ".ts")) {
84
87
  const mod = await import(pathToFileURL(f).href);
85
- if (mod.default) channels.push(mod.default as Channel);
88
+ if (mod.default) channels.push(resolveChannel(mod.default as Channel | ChannelFactory, process.env));
86
89
  }
87
90
 
88
91
  // connections/*.ts — outbound: wire external MCP/OpenAPI servers, turning their