@junejs/server 0.0.16 → 0.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
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",
@@ -30,8 +30,8 @@
30
30
  "typecheck": "tsc --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@junejs/core": "0.0.16",
34
- "@junejs/db": "0.0.16",
33
+ "@junejs/core": "0.0.18",
34
+ "@junejs/db": "0.0.18",
35
35
  "marked": "^18.0.5",
36
36
  "rolldown": "^1.1.0",
37
37
  "postcss": "^8.4.0",
@@ -40,10 +40,30 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": "^19.2.0",
43
- "react-dom": "^19.2.0"
43
+ "react-dom": "^19.2.0",
44
+ "pg": "^8.0.0",
45
+ "mysql2": "^3.0.0",
46
+ "@libsql/client": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "pg": {
50
+ "optional": true
51
+ },
52
+ "mysql2": {
53
+ "optional": true
54
+ },
55
+ "@libsql/client": {
56
+ "optional": true
57
+ }
44
58
  },
45
59
  "repository": {
46
60
  "type": "git",
47
61
  "url": "https://github.com/junebuild/june"
62
+ },
63
+ "devDependencies": {
64
+ "@libsql/client": "^0.17.3",
65
+ "@types/pg": "^8.20.0",
66
+ "mysql2": "^3.22.5",
67
+ "pg": "^8.21.0"
48
68
  }
49
69
  }
package/src/adapter.ts CHANGED
@@ -136,47 +136,72 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
136
136
 
137
137
  // Vercel target — emits the Build Output API v3 tree (.vercel/output/). June's
138
138
  // worker is already a Web-standard fetch handler, so this is packaging, not a
139
- // re-bundle: the same portable graph, built with edge-light conditions, becomes a
140
- // Vercel Edge Function. v1 scope: SSR + static + the full agent surface. No db
141
- // (D1 is Cloudflare-only) — declaring one fails fast; an HTTP-driver db backend
142
- // for Vercel is a planned follow-up.
139
+ // re-bundle: the same portable graph (built with edge-light conditions, so
140
+ // react-dom resolves to server.edge.js) packaged for the platform.
141
+ //
142
+ // Default runtime is "node" — a Vercel Function on Fluid compute. Vercel
143
+ // deprecated the Edge Functions *product* and officially recommends the Node
144
+ // runtime for performance + reliability, and Fluid compute (in-function
145
+ // concurrency) closed the cold-start gap edge used to win on. Node also lifts
146
+ // edge's limits (no 1–4MB code cap, full API). `vercel({ runtime: "edge" })` still
147
+ // emits an Edge Function for latency-critical global cases — the runtime is not
148
+ // removed, just discouraged. BOTH run the SAME bundle; only the .func packaging
149
+ // (.vc-config.json + the export shape) differs.
150
+ //
151
+ // db: turso() works (libsql over HTTPS — the build bundles the web client, the
152
+ // request opens it from env → ambient `import { db }`). sqlite()/d1() are
153
+ // Cloudflare-only (a D1 binding); validate() rejects them with that guidance.
143
154
  //
144
155
  // Routing: only the hashed framework assets (/_june/*) are served statically by
145
156
  // the CDN (their exact-path URLs match June's scheme); every page + projection
146
- // (.md/.json/mcp/llms.txt) renders through the one edge function, so the
147
- // dual-audience surface is identical to Workers.
157
+ // (.md/.json/mcp/llms.txt) renders through the one function, so the dual-audience
158
+ // surface is identical to Workers.
148
159
  const VERCEL_FUNCTION = "__june"; // → /__june, the catch-all SSR function
149
160
 
150
- export function vercel(opts?: { regions?: string[] }): JuneAdapter {
161
+ export function vercel(opts?: { runtime?: "node" | "edge"; regions?: string[] }): JuneAdapter {
162
+ const isEdge = (opts?.runtime ?? "node") === "edge";
151
163
  return {
152
164
  name: "vercel",
153
- capabilities: { runtime: "edge", persistentConnections: false, assets: "platform" },
154
- // NO "worker"/"browser": react-dom's ./server exports map "worker" → the
155
- // BROWSER SSR build (server.browser.js), listed BEFORE "edge-light", so
156
- // requesting "worker" pulls the wrong renderer and the edge function crashes
157
- // at invocation. "edge-light" → server.edge.js (the same build workerd gets
158
- // via "workerd"). Mirrors workers()'s shape: <runtime>, edge, import, default.
165
+ capabilities: {
166
+ // Node on Fluid holds streamed/SSE connections; the edge variant keeps the
167
+ // conservative flag it shipped with.
168
+ runtime: isEdge ? "edge" : "node",
169
+ persistentConnections: !isEdge,
170
+ assets: "platform",
171
+ },
172
+ // The bundle is identical for both runtimes. NO "worker"/"browser": react-dom's
173
+ // ./server exports map "worker" → the BROWSER SSR build (server.browser.js),
174
+ // listed BEFORE "edge-light", so requesting "worker" pulls the wrong renderer.
175
+ // "edge-light" → server.edge.js, which uses Web Streams (renderToReadableStream)
176
+ // and runs on workerd AND Node alike. Mirrors workers()'s shape.
159
177
  conditions: ["edge-light", "edge", "import", "default"],
160
178
 
161
- validate({ plan }) {
162
- if (plan.db) {
179
+ validate({ config }) {
180
+ // turso() is libsql over HTTPS — it runs on Vercel (the build bundles the web
181
+ // client; the request opens it from env → ambient `import { db }`). sqlite()/
182
+ // d1() need a D1 binding, which is Cloudflare-only.
183
+ const kind = config.resources?.db?.kind;
184
+ if (kind && kind !== "turso") {
163
185
  throw new Error(
164
- "vercel(): the Vercel adapter has no db backend yet (D1 is Cloudflare-only).\n" +
165
- " Remove the `db` resource from june.config.ts, or deploy to Workers.\n" +
166
- " An HTTP-driver db (Neon/Turso) for Vercel is planned.",
186
+ `vercel(): the '${kind}' db isn't supported on Vercel (D1 is Cloudflare-only).\n` +
187
+ " Use turso() (libsql over HTTPS) for a Vercel db, or deploy to Workers.",
167
188
  );
168
189
  }
169
190
  },
170
191
 
171
192
  entry() {
172
193
  // No withAssets: the CDN serves /_june/*; this function renders everything
173
- // else. Resources arrive via process.env (Vercel injects env vars into the
174
- // edge runtime); there are no platform bindings.
194
+ // else. Resources arrive via process.env (Vercel injects env vars); there
195
+ // are no platform bindings. The Node runtime captures the `fetch` Web
196
+ // Standard export (an object with a fetch method); the Edge runtime takes a
197
+ // bare default function. Same pipeline call either way.
198
+ const env = `const __env = typeof process !== "undefined" && process.env ? process.env : {};\n`;
175
199
  return {
176
200
  imports: [],
177
201
  wrap: (pipelineVar) =>
178
- `const __env = typeof process !== "undefined" && process.env ? process.env : {};\n` +
179
- `export default (request) => ${pipelineVar}.fetch(request, __env);`,
202
+ isEdge
203
+ ? env + `export default (request) => ${pipelineVar}.fetch(request, __env);`
204
+ : env + `export default { fetch: (request) => ${pipelineVar}.fetch(request, __env) };`,
180
205
  };
181
206
  },
182
207
 
@@ -192,14 +217,28 @@ export function vercel(opts?: { regions?: string[] }): JuneAdapter {
192
217
  // function ("referencing unsupported modules"). assets/ is handled below.
193
218
  const jsFiles = (await readdir(outDir)).filter((f) => f.endsWith(".js"));
194
219
  for (const f of jsFiles) await copyFile(join(outDir, f), join(fnDir, f));
195
- await writeFile(
196
- join(fnDir, ".vc-config.json"),
197
- JSON.stringify(
198
- { runtime: "edge", entrypoint: "worker.js", ...(opts?.regions ? { regions: opts.regions } : {}) },
199
- null,
200
- 2,
201
- ) + "\n",
202
- );
220
+
221
+ const regions = opts?.regions ? { regions: opts.regions } : {};
222
+ const vcConfig = isEdge
223
+ ? { runtime: "edge", entrypoint: "worker.js", ...regions }
224
+ : {
225
+ // A Vercel Function on Fluid compute. handler = the ESM bundle entry;
226
+ // launcherType Nodejs invokes its default `fetch` export.
227
+ // supportsResponseStreaming is REQUIRED or a streamed Response (our SSE
228
+ // live feed, streamed SSR) is buffered instead of flushed.
229
+ runtime: "nodejs22.x",
230
+ handler: "worker.js",
231
+ launcherType: "Nodejs",
232
+ supportsResponseStreaming: true,
233
+ ...regions,
234
+ };
235
+ await writeFile(join(fnDir, ".vc-config.json"), JSON.stringify(vcConfig, null, 2) + "\n");
236
+ // The bundle is ESM (worker.js + ./chunk-*.js all use `export`/`import`).
237
+ // The Node runtime loads the .func as CommonJS by default, so mark the whole
238
+ // function dir as ESM. (The edge runtime always treats sources as ESM.)
239
+ if (!isEdge) {
240
+ await writeFile(join(fnDir, "package.json"), JSON.stringify({ type: "module" }, null, 2) + "\n");
241
+ }
203
242
 
204
243
  // static: only the hashed framework assets under /_june/ (exact-path URLs)
205
244
  if (hasAssets) {
package/src/build.ts CHANGED
@@ -378,12 +378,25 @@ export async function juneBuild(
378
378
  kv: !!resourcesCfg?.kv,
379
379
  blob: !!resourcesCfg?.blob,
380
380
  };
381
- if (hasResources) {
381
+ // Two SQLite-dialect defaults, picked by the declared db's kind:
382
+ // turso() → libsql over HTTPS, connected from env (TURSO_*) via the
383
+ // bundled web client. Open the declared factory directly so it
384
+ // feeds the ambient `import { db }` scope (re-emitted by kind,
385
+ // not imported from the app config, to avoid the host barrel).
386
+ // sqlite()/d1() → a D1 binding from env (env.DB), via bindWorkerResources.
387
+ // (kv/blob on a turso deploy aren't wired yet — db-only.)
388
+ const tursoDb = resourcesCfg?.db?.kind === "turso";
389
+ if (tursoDb) {
390
+ imports.push(`import { turso } from "@junejs/server/db";`);
391
+ imports.push(`import { memoizeResources } from "@junejs/server/resources";`);
392
+ } else if (hasResources) {
382
393
  imports.push(`import { bindWorkerResources } from "@junejs/server/resources";`);
383
394
  }
384
- const resourcesField = hasResources
385
- ? `\n resources: bindWorkerResources(${JSON.stringify(resourceFlags)}),`
386
- : "";
395
+ const resourcesField = tursoDb
396
+ ? `\n resources: memoizeResources({ db: turso() }),`
397
+ : hasResources
398
+ ? `\n resources: bindWorkerResources(${JSON.stringify(resourceFlags)}),`
399
+ : "";
387
400
 
388
401
  // Opt-in Tier-3 data layer: import its installDataLayer from the declared module
389
402
  // and call it at worker boot — the prod twin of the dev host's dataLayer.install()
@@ -521,10 +534,12 @@ ${adapterEntry.wrap("pipeline")}
521
534
  ? (JSON.parse(await Bun.file(pkgPath).text()) as { name?: string }).name
522
535
  : undefined;
523
536
  const defaultName = (pkgName ?? basename(appRoot)).replace(/[^a-z0-9-]/gi, "-").toLowerCase();
524
- // A declared `db` → a D1 binding named DB (the name bindWorkerResources reads).
525
- const plan: ResourcePlan = resourcesCfg?.db
526
- ? { db: { binding: "DB", databaseName: `${defaultName}-db` } }
527
- : {};
537
+ // A declared D1-backed db (sqlite/d1) → a D1 binding named DB (the name
538
+ // bindWorkerResources reads). turso() connects from env, not a binding — no plan.
539
+ const plan: ResourcePlan =
540
+ resourcesCfg?.db && resourcesCfg.db.kind !== "turso"
541
+ ? { db: { binding: "DB", databaseName: `${defaultName}-db` } }
542
+ : {};
528
543
  await adapter.emit({ appRoot, outDir, hasAssets, linkHeader, config: fullConfig, plan, defaultName });
529
544
  if (plan.db) {
530
545
  console.log(
package/src/d1.ts CHANGED
@@ -26,6 +26,7 @@ export interface D1Database {
26
26
  // already-bound D1Database.
27
27
  export function d1(binding: D1Database): DbFactory {
28
28
  const db: JuneDb = {
29
+ dialect: "sqlite", // D1 *is* sqlite — same SQL, same Juno compiler
29
30
  async query<T>(sql: string, params: unknown[] = []) {
30
31
  return (await binding.prepare(sql).bind(...params).all<T>()).results;
31
32
  },
package/src/db.ts CHANGED
@@ -1,33 +1,38 @@
1
1
  // `db` resource adapters — implementations of the @junejs/core JuneDb contract.
2
2
  // Declared in june.config.ts (`resources.db: sqlite(...)` / `d1(...)`), opened
3
- // by the host, injected onto RouteContext as `ctx.db`. The framework depends on
4
- // the JuneDb contract; these adapters (and Juno on top) are swappable.
3
+ // by the host and made ambient (`import { db } from "@junejs/db"`, via
4
+ // runInScope — NOT on ctx). The framework depends on the JuneDb contract; these
5
+ // adapters (and Juno on top) are swappable.
5
6
  //
6
- // sqlite() is host-coupled (node:fs + ./host) and DEV-only. The edge-safe d1()
7
- // adapter lives in ./d1 so the generated worker can import it without dragging
8
- // this module's host imports into the workerd bundle; it is re-exported here so
9
- // `@junejs/server/db` stays the one place both db adapters are named.
10
-
11
- import { mkdirSync } from "node:fs";
12
- import { dirname } from "node:path";
7
+ // This module is HOST-FREE so the generated worker can import it (e.g. `turso`)
8
+ // without dragging node:fs / the dev host into the bundle (reminder #4). The two
9
+ // host-coupled adapters keep their host touch out of the static graph:
10
+ // - d1() lives in ./d1 (edge-safe; env binding).
11
+ // - sqlite() is DEV-only and imports node:fs + ./host LAZILY, inside open().
12
+ // All five (sqlite/d1/postgres/mysql/turso) are named from this one place.
13
13
 
14
14
  import type { DbFactory } from "@junejs/core/resources";
15
15
 
16
- import { host } from "./host";
17
-
18
16
  export { d1, type D1Database } from "./d1";
17
+ export { postgres, pgJuneDb, type PgClientLike } from "./postgres-driver";
18
+ export { mysql, mysqlJuneDb, type MysqlConnLike } from "./mysql-driver";
19
+ export { turso, libsqlJuneDb, type LibsqlClientLike } from "./turso-driver";
19
20
 
20
21
  // Local SQLite — the zero-config dev default. A persistent FILE, not :memory:,
21
22
  // on purpose: `june dev` restarts the process on every save (the watch
22
23
  // supervisor), so an in-memory default would evaporate your dev data at each
23
24
  // edit. A file also makes the dev db inspectable with the sqlite3 every
24
25
  // machine already has (`sqlite3 .june/dev.sqlite`). Pass ":memory:" explicitly
25
- // for ephemerality.
26
+ // for ephemerality. node:fs + ./host are imported lazily so this module stays
27
+ // host-free (sqlite() only runs in dev, where the imports resolve).
26
28
  export function sqlite(opts: { path?: string } = {}): DbFactory {
27
29
  const path = opts.path ?? ".june/dev.sqlite";
28
30
  return {
29
31
  kind: "sqlite",
30
- open: () => {
32
+ open: async () => {
33
+ const { mkdirSync } = await import("node:fs");
34
+ const { dirname } = await import("node:path");
35
+ const { host } = await import("./host");
31
36
  if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
32
37
  return host.openDb(path);
33
38
  },
package/src/host.ts CHANGED
@@ -49,7 +49,7 @@ export interface JuneHost {
49
49
  // this with the in-isolate dual-graph loader; the seam stays for the fallback.
50
50
  spawnModule(entry: string, args: string[], opts: { conditions?: string[] }): SpawnedModule;
51
51
  // Open a LOCAL SQLite database — the internal primitive the `sqlite()` db
52
- // adapter builds on (docs/data-layer-boundary.md: openDb is demoted from the
52
+ // adapter builds on (docs/data-layer.md: openDb is demoted from the
53
53
  // user-facing API to a host primitive; apps declare `resources.db` instead).
54
54
  openDb(path: string): Promise<JuneDb>;
55
55
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export {
11
11
  type AdapterEmitContext,
12
12
  } from "./adapter";
13
13
  export { host, type JuneHost, type JuneDb, type RunResult, type ServeHandle, type SpawnedModule } from "./host";
14
- export { sqlite, d1, type D1Database } from "./db";
14
+ export { sqlite, d1, postgres, mysql, turso, type D1Database } from "./db";
15
15
  export { memoryKv, redisKv } from "./kv";
16
16
  export { localBlob, r2, type R2Bucket } from "./blob";
17
17
  export {
package/src/kv.ts CHANGED
@@ -1,4 +1,4 @@
1
- // `kv` resource adapters. The kv resource IS the cache (docs/data-layer-boundary
1
+ // `kv` resource adapters. The kv resource IS the cache (docs/data-layer
2
2
  // .md: "reframe cache as the kv resource — it already is one"): these wrap
3
3
  // @junejs/core/cache's CacheStore (memory / redis) as the simpler JuneKv contract,
4
4
  // so there is ONE key-value system, surfaced both as cache() and as ambient kv.
@@ -0,0 +1,68 @@
1
+ // MySQL `db` adapter — implements the @junejs/core JuneDb contract over `mysql2`.
2
+ // Tags itself `dialect: "mysql"` so Juno compiles backtick / `?` / ON DUPLICATE KEY
3
+ // SQL for it. `mysql2` is dynamically imported (a peer the app installs only when it
4
+ // declares `mysql()`). Test/dev grade: ONE connection per open(); pooling / env
5
+ // binding come later.
6
+
7
+ import type { DbFactory, JuneDb, RunResult } from "@junejs/core/resources";
8
+
9
+ // mysql2 returns [rows, fields]; a write yields a ResultSetHeader as the first element.
10
+ interface OkHeader {
11
+ affectedRows?: number;
12
+ insertId?: number;
13
+ }
14
+ // The slice of mysql2/promise's Connection we depend on — so mysqlJuneDb is fakeable.
15
+ export interface MysqlConnLike {
16
+ query(sql: string, params?: unknown[]): Promise<[unknown, unknown]>;
17
+ end(): Promise<void>;
18
+ }
19
+
20
+ // Wrap a mysql2 connection as a JuneDb. Exported for offline unit tests.
21
+ export function mysqlJuneDb(conn: MysqlConnLike): JuneDb {
22
+ const self: JuneDb = {
23
+ dialect: "mysql",
24
+ async query<T>(sql: string, params: unknown[] = []) {
25
+ return (await conn.query(sql, params))[0] as T[];
26
+ },
27
+ async get<T>(sql: string, params: unknown[] = []) {
28
+ const rows = (await conn.query(sql, params))[0] as unknown[];
29
+ return (rows[0] ?? undefined) as T | undefined;
30
+ },
31
+ async run(sql: string, params: unknown[] = []): Promise<RunResult> {
32
+ const header = (await conn.query(sql, params))[0] as OkHeader;
33
+ return { changes: header.affectedRows ?? 0, lastInsertRowid: header.insertId ?? 0 };
34
+ },
35
+ async exec(sql: string) {
36
+ await conn.query(sql); // needs multipleStatements for a multi-statement script
37
+ },
38
+ async transaction<T>(fn: (tx: JuneDb) => Promise<T>) {
39
+ await conn.query("begin");
40
+ try {
41
+ const out = await fn(self);
42
+ await conn.query("commit");
43
+ return out;
44
+ } catch (e) {
45
+ await conn.query("rollback");
46
+ throw e;
47
+ }
48
+ },
49
+ async close() {
50
+ await conn.end();
51
+ },
52
+ };
53
+ return self;
54
+ }
55
+
56
+ // `mysql({ url })` — multipleStatements is on so exec() can run a whole migration file.
57
+ export function mysql(opts: { url: string }): DbFactory {
58
+ return {
59
+ kind: "mysql",
60
+ open: async () => {
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ const mod = (await import("mysql2/promise")) as any;
63
+ const create = (mod.default ?? mod).createConnection;
64
+ const conn = await create({ uri: opts.url, multipleStatements: true });
65
+ return mysqlJuneDb(conn);
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,67 @@
1
+ // Postgres `db` adapter — implements the @junejs/core JuneDb contract over
2
+ // node-postgres (`pg`). Tags itself `dialect: "postgres"` so Juno compiles `$n` /
3
+ // double-quoted SQL for it (see @junejs/juno's dialectFor). `pg` is dynamically
4
+ // imported, so it's a peer the app installs only when it declares `postgres()` — the
5
+ // host never hard-depends on it. Test/dev grade: ONE connection per open(); pooling /
6
+ // env binding / deploy adapters come in a later step.
7
+
8
+ import type { DbFactory, JuneDb, RunResult } from "@junejs/core/resources";
9
+
10
+ // The slice of `pg`'s Client we depend on — so pgJuneDb is unit-testable with a fake.
11
+ export interface PgClientLike {
12
+ query(sql: string, params?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
13
+ end(): Promise<void>;
14
+ }
15
+
16
+ // Wrap a connected pg client as a JuneDb. Exported (not just the factory) so the
17
+ // mapping — rows, first-row, RunResult, transaction ordering — is testable offline.
18
+ export function pgJuneDb(client: PgClientLike): JuneDb {
19
+ const self: JuneDb = {
20
+ dialect: "postgres",
21
+ async query<T>(sql: string, params: unknown[] = []) {
22
+ return (await client.query(sql, params)).rows as T[];
23
+ },
24
+ async get<T>(sql: string, params: unknown[] = []) {
25
+ return ((await client.query(sql, params)).rows[0] ?? undefined) as T | undefined;
26
+ },
27
+ async run(sql: string, params: unknown[] = []): Promise<RunResult> {
28
+ const r = await client.query(sql, params);
29
+ // Postgres has no autoincrement rowid — callers use `RETURNING id`. Report the
30
+ // affected-row count; lastInsertRowid is 0 (not meaningful on PG).
31
+ return { changes: r.rowCount ?? 0, lastInsertRowid: 0 };
32
+ },
33
+ async exec(sql: string) {
34
+ await client.query(sql); // DDL / multi-statement via the simple-query protocol
35
+ },
36
+ async transaction<T>(fn: (tx: JuneDb) => Promise<T>) {
37
+ await client.query("begin");
38
+ try {
39
+ const out = await fn(self); // same connection
40
+ await client.query("commit");
41
+ return out;
42
+ } catch (e) {
43
+ await client.query("rollback");
44
+ throw e;
45
+ }
46
+ },
47
+ async close() {
48
+ await client.end();
49
+ },
50
+ };
51
+ return self;
52
+ }
53
+
54
+ // `postgres({ url })` — declared in june.config.ts; opened by the host.
55
+ export function postgres(opts: { url: string }): DbFactory {
56
+ return {
57
+ kind: "postgres",
58
+ open: async () => {
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const mod = (await import("pg")) as any;
61
+ const Client = (mod.default ?? mod).Client;
62
+ const client = new Client({ connectionString: opts.url });
63
+ await client.connect();
64
+ return pgJuneDb(client);
65
+ },
66
+ };
67
+ }
package/src/resources.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  // Resource resolution — open the handles declared in june.config.ts `resources`
2
- // and hand them to the pipeline, which injects them onto RouteContext (ctx.db /
3
- // ctx.kv / ctx.blob). A resource not declared is never opened (and, for static
4
- // apps, tree-shaken out — the build freeze knows which routes touch resources).
2
+ // and hand them to the pipeline, which runs each request inside runInScope() so
3
+ // they resolve AMBIENTLY (`import { db } from "@junejs/db"`). ctx never carries
4
+ // them — ctx is identity-only (see route.ts). A resource not declared is never
5
+ // opened (and, for static apps, tree-shaken out — the build freeze knows which
6
+ // routes touch resources).
5
7
 
6
8
  import type { ResourceConfig, Resources } from "@junejs/core/resources";
7
9
 
@@ -48,7 +50,7 @@ export type ResourceFlags = { db?: boolean; kv?: boolean; blob?: boolean };
48
50
  // from env (env.DB → D1) — the prod half of "sqlite dev → D1 prod, one
49
51
  // declaration": the same `resources: { db: sqlite() }` runs on D1 at the edge,
50
52
  // because D1 *is* SQLite (same SQL, same Juno tables). No env binding → the
51
- // handle is absent and the route degrades (ctx.db undefined), never a crash.
53
+ // handle is absent and the route degrades (ambient `db` unavailable), never a crash.
52
54
  // Memoized per isolate (env is stable across requests in a workerd isolate).
53
55
  //
54
56
  // env is typed `unknown` (it arrives untyped from the runtime) and narrowed to
@@ -51,6 +51,7 @@ function asyncSqlite(db: SyncSqlite): JuneDb {
51
51
  return s;
52
52
  };
53
53
  const self: JuneDb = {
54
+ dialect: "sqlite",
54
55
  async query<T>(sql: string, params: unknown[] = []) {
55
56
  return prep(sql).all(...params) as T[];
56
57
  },
@@ -0,0 +1,96 @@
1
+ // Turso / libsql `db` adapter — implements the @junejs/core JuneDb contract over
2
+ // @libsql/client. Turso speaks libsql (SQLite over an HTTPS endpoint), so it tags
3
+ // itself `dialect: "sqlite"`: Juno compiles the exact same `?`-placeholder SQLite
4
+ // SQL it uses for the local sqlite() adapter, and the connection is a plain URL +
5
+ // token (no TCP socket, no platform binding) — so it runs on the Vercel Node
6
+ // runtime, on edge, and anywhere with fetch. `@libsql/client` is dynamically
7
+ // imported: a peer the app installs only when it declares turso(); the host never
8
+ // hard-depends on it. Host-free (no node:* / ./host) so it can enter a worker
9
+ // bundle, unlike sqlite().
10
+
11
+ import type { DbFactory, JuneDb, RunResult } from "@junejs/core/resources";
12
+
13
+ // The slice of @libsql/client's Client we depend on — so libsqlJuneDb is
14
+ // unit-testable with a fake (no network).
15
+ export interface LibsqlResultSet {
16
+ rows: unknown[];
17
+ rowsAffected: number;
18
+ lastInsertRowid?: bigint | number;
19
+ }
20
+ export interface LibsqlClientLike {
21
+ execute(stmt: { sql: string; args?: unknown[] } | string): Promise<LibsqlResultSet>;
22
+ executeMultiple?(sql: string): Promise<void>;
23
+ close(): void | Promise<void>;
24
+ }
25
+
26
+ // Wrap a libsql client as a JuneDb. Exported (not just the factory) so the mapping
27
+ // — rows, first-row, RunResult, transaction ordering — is testable offline.
28
+ export function libsqlJuneDb(client: LibsqlClientLike): JuneDb {
29
+ const self: JuneDb = {
30
+ dialect: "sqlite", // libsql IS sqlite — same SQL + Juno tables as sqlite()/d1()
31
+ async query<T>(sql: string, params: unknown[] = []) {
32
+ return (await client.execute({ sql, args: params })).rows as T[];
33
+ },
34
+ async get<T>(sql: string, params: unknown[] = []) {
35
+ return ((await client.execute({ sql, args: params })).rows[0] ?? undefined) as T | undefined;
36
+ },
37
+ async run(sql: string, params: unknown[] = []): Promise<RunResult> {
38
+ const r = await client.execute({ sql, args: params });
39
+ // libsql returns lastInsertRowid as a bigint; RunResult is number-ish.
40
+ return { changes: r.rowsAffected ?? 0, lastInsertRowid: Number(r.lastInsertRowid ?? 0) };
41
+ },
42
+ async exec(sql: string) {
43
+ // DDL / migrations are multi-statement; executeMultiple splits on `;`.
44
+ if (client.executeMultiple) await client.executeMultiple(sql);
45
+ else await client.execute(sql);
46
+ },
47
+ async transaction<T>(fn: (tx: JuneDb) => Promise<T>) {
48
+ // Test/dev grade, matching postgres()/mysql(): BEGIN/COMMIT on the one
49
+ // client. (For strict isolation over HTTP, libsql's interactive
50
+ // client.transaction() is the upgrade path.)
51
+ await client.execute("BEGIN");
52
+ try {
53
+ const out = await fn(self);
54
+ await client.execute("COMMIT");
55
+ return out;
56
+ } catch (e) {
57
+ await client.execute("ROLLBACK");
58
+ throw e;
59
+ }
60
+ },
61
+ async close() {
62
+ await client.close();
63
+ },
64
+ };
65
+ return self;
66
+ }
67
+
68
+ // `turso({ url, authToken })` — declared in june.config.ts, or omit both to read
69
+ // the standard TURSO_DATABASE_URL / TURSO_AUTH_TOKEN env vars at open() time
70
+ // (NOT at construction — so the build can evaluate the config without the secrets
71
+ // present, and the deployed function reads them from its own environment).
72
+ export function turso(opts: { url?: string; authToken?: string } = {}): DbFactory {
73
+ return {
74
+ kind: "turso",
75
+ open: async () => {
76
+ const url = opts.url ?? readEnv("TURSO_DATABASE_URL");
77
+ const authToken = opts.authToken ?? readEnv("TURSO_AUTH_TOKEN");
78
+ if (!url) {
79
+ throw new Error(
80
+ "turso(): no database url — pass { url } or set TURSO_DATABASE_URL in the environment.",
81
+ );
82
+ }
83
+ // The pure-fetch web client: works in dev (Bun) AND bundles into the one
84
+ // deployed function (the node client would pull native bindings). Turso is
85
+ // remote (libsql:// over HTTPS), which is exactly what the web client speaks.
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const mod = (await import("@libsql/client/web")) as any;
88
+ const createClient = (mod.default ?? mod).createClient;
89
+ return libsqlJuneDb(createClient({ url, authToken }));
90
+ },
91
+ };
92
+ }
93
+
94
+ function readEnv(name: string): string | undefined {
95
+ return typeof process !== "undefined" && process.env ? process.env[name] : undefined;
96
+ }