@junejs/server 0.0.17 → 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 +9 -4
- package/src/adapter.ts +12 -9
- package/src/build.ts +23 -8
- package/src/db.ts +16 -13
- package/src/host.ts +1 -1
- package/src/index.ts +1 -1
- package/src/kv.ts +1 -1
- package/src/resources.ts +6 -4
- package/src/turso-driver.ts +96 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
34
|
-
"@junejs/db": "0.0.
|
|
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",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"react": "^19.2.0",
|
|
43
43
|
"react-dom": "^19.2.0",
|
|
44
44
|
"pg": "^8.0.0",
|
|
45
|
-
"mysql2": "^3.0.0"
|
|
45
|
+
"mysql2": "^3.0.0",
|
|
46
|
+
"@libsql/client": "^0.14.0 || ^0.15.0 || ^0.16.0 || ^0.17.0"
|
|
46
47
|
},
|
|
47
48
|
"peerDependenciesMeta": {
|
|
48
49
|
"pg": {
|
|
@@ -50,6 +51,9 @@
|
|
|
50
51
|
},
|
|
51
52
|
"mysql2": {
|
|
52
53
|
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@libsql/client": {
|
|
56
|
+
"optional": true
|
|
53
57
|
}
|
|
54
58
|
},
|
|
55
59
|
"repository": {
|
|
@@ -57,6 +61,7 @@
|
|
|
57
61
|
"url": "https://github.com/junebuild/june"
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
64
|
+
"@libsql/client": "^0.17.3",
|
|
60
65
|
"@types/pg": "^8.20.0",
|
|
61
66
|
"mysql2": "^3.22.5",
|
|
62
67
|
"pg": "^8.21.0"
|
package/src/adapter.ts
CHANGED
|
@@ -143,14 +143,14 @@ export function workers(opts?: { name?: string; domain?: string }): JuneAdapter
|
|
|
143
143
|
// deprecated the Edge Functions *product* and officially recommends the Node
|
|
144
144
|
// runtime for performance + reliability, and Fluid compute (in-function
|
|
145
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)
|
|
147
|
-
// HTTP/TCP db backend (Neon/Turso) needs. `vercel({ runtime: "edge" })` still
|
|
146
|
+
// edge's limits (no 1–4MB code cap, full API). `vercel({ runtime: "edge" })` still
|
|
148
147
|
// emits an Edge Function for latency-critical global cases — the runtime is not
|
|
149
148
|
// removed, just discouraged. BOTH run the SAME bundle; only the .func packaging
|
|
150
149
|
// (.vc-config.json + the export shape) differs.
|
|
151
150
|
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
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.
|
|
154
154
|
//
|
|
155
155
|
// Routing: only the hashed framework assets (/_june/*) are served statically by
|
|
156
156
|
// the CDN (their exact-path URLs match June's scheme); every page + projection
|
|
@@ -176,12 +176,15 @@ export function vercel(opts?: { runtime?: "node" | "edge"; regions?: string[] })
|
|
|
176
176
|
// and runs on workerd AND Node alike. Mirrors workers()'s shape.
|
|
177
177
|
conditions: ["edge-light", "edge", "import", "default"],
|
|
178
178
|
|
|
179
|
-
validate({
|
|
180
|
-
|
|
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") {
|
|
181
185
|
throw new Error(
|
|
182
|
-
|
|
183
|
-
"
|
|
184
|
-
" 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.",
|
|
185
188
|
);
|
|
186
189
|
}
|
|
187
190
|
},
|
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
|
-
|
|
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 =
|
|
385
|
-
? `\n resources:
|
|
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
|
|
525
|
-
|
|
526
|
-
|
|
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/db.ts
CHANGED
|
@@ -1,35 +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
|
|
4
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
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";
|
|
19
17
|
export { postgres, pgJuneDb, type PgClientLike } from "./postgres-driver";
|
|
20
18
|
export { mysql, mysqlJuneDb, type MysqlConnLike } from "./mysql-driver";
|
|
19
|
+
export { turso, libsqlJuneDb, type LibsqlClientLike } from "./turso-driver";
|
|
21
20
|
|
|
22
21
|
// Local SQLite — the zero-config dev default. A persistent FILE, not :memory:,
|
|
23
22
|
// on purpose: `june dev` restarts the process on every save (the watch
|
|
24
23
|
// supervisor), so an in-memory default would evaporate your dev data at each
|
|
25
24
|
// edit. A file also makes the dev db inspectable with the sqlite3 every
|
|
26
25
|
// machine already has (`sqlite3 .june/dev.sqlite`). Pass ":memory:" explicitly
|
|
27
|
-
// 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).
|
|
28
28
|
export function sqlite(opts: { path?: string } = {}): DbFactory {
|
|
29
29
|
const path = opts.path ?? ".june/dev.sqlite";
|
|
30
30
|
return {
|
|
31
31
|
kind: "sqlite",
|
|
32
|
-
open: () => {
|
|
32
|
+
open: async () => {
|
|
33
|
+
const { mkdirSync } = await import("node:fs");
|
|
34
|
+
const { dirname } = await import("node:path");
|
|
35
|
+
const { host } = await import("./host");
|
|
33
36
|
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
34
37
|
return host.openDb(path);
|
|
35
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
|
|
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, postgres, mysql, 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
|
|
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.
|
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
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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 (
|
|
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
|
|
@@ -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
|
+
}
|