@junejs/server 0.0.15 → 0.0.17

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.15",
3
+ "version": "0.0.17",
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.15",
34
- "@junejs/db": "0.0.15",
33
+ "@junejs/core": "0.0.17",
34
+ "@junejs/db": "0.0.17",
35
35
  "marked": "^18.0.5",
36
36
  "rolldown": "^1.1.0",
37
37
  "postcss": "^8.4.0",
@@ -40,10 +40,25 @@
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
+ },
47
+ "peerDependenciesMeta": {
48
+ "pg": {
49
+ "optional": true
50
+ },
51
+ "mysql2": {
52
+ "optional": true
53
+ }
44
54
  },
45
55
  "repository": {
46
56
  "type": "git",
47
57
  "url": "https://github.com/junebuild/june"
58
+ },
59
+ "devDependencies": {
60
+ "@types/pg": "^8.20.0",
61
+ "mysql2": "^3.22.5",
62
+ "pg": "^8.21.0"
48
63
  }
49
64
  }
package/src/adapter.ts CHANGED
@@ -136,26 +136,44 @@ 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) and is the substrate a future
147
+ // HTTP/TCP db backend (Neon/Turso) needs. `vercel({ runtime: "edge" })` still
148
+ // emits an Edge Function for latency-critical global cases — the runtime is not
149
+ // removed, just discouraged. BOTH run the SAME bundle; only the .func packaging
150
+ // (.vc-config.json + the export shape) differs.
151
+ //
152
+ // No db yet (D1 is Cloudflare-only) — declaring one still fails fast; the Node
153
+ // runtime makes a real backend possible but it isn't wired here.
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
179
  validate({ plan }) {
@@ -170,13 +188,17 @@ export function vercel(opts?: { regions?: string[] }): JuneAdapter {
170
188
 
171
189
  entry() {
172
190
  // 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.
191
+ // else. Resources arrive via process.env (Vercel injects env vars); there
192
+ // are no platform bindings. The Node runtime captures the `fetch` Web
193
+ // Standard export (an object with a fetch method); the Edge runtime takes a
194
+ // bare default function. Same pipeline call either way.
195
+ const env = `const __env = typeof process !== "undefined" && process.env ? process.env : {};\n`;
175
196
  return {
176
197
  imports: [],
177
198
  wrap: (pipelineVar) =>
178
- `const __env = typeof process !== "undefined" && process.env ? process.env : {};\n` +
179
- `export default (request) => ${pipelineVar}.fetch(request, __env);`,
199
+ isEdge
200
+ ? env + `export default (request) => ${pipelineVar}.fetch(request, __env);`
201
+ : env + `export default { fetch: (request) => ${pipelineVar}.fetch(request, __env) };`,
180
202
  };
181
203
  },
182
204
 
@@ -192,14 +214,28 @@ export function vercel(opts?: { regions?: string[] }): JuneAdapter {
192
214
  // function ("referencing unsupported modules"). assets/ is handled below.
193
215
  const jsFiles = (await readdir(outDir)).filter((f) => f.endsWith(".js"));
194
216
  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
- );
217
+
218
+ const regions = opts?.regions ? { regions: opts.regions } : {};
219
+ const vcConfig = isEdge
220
+ ? { runtime: "edge", entrypoint: "worker.js", ...regions }
221
+ : {
222
+ // A Vercel Function on Fluid compute. handler = the ESM bundle entry;
223
+ // launcherType Nodejs invokes its default `fetch` export.
224
+ // supportsResponseStreaming is REQUIRED or a streamed Response (our SSE
225
+ // live feed, streamed SSR) is buffered instead of flushed.
226
+ runtime: "nodejs22.x",
227
+ handler: "worker.js",
228
+ launcherType: "Nodejs",
229
+ supportsResponseStreaming: true,
230
+ ...regions,
231
+ };
232
+ await writeFile(join(fnDir, ".vc-config.json"), JSON.stringify(vcConfig, null, 2) + "\n");
233
+ // The bundle is ESM (worker.js + ./chunk-*.js all use `export`/`import`).
234
+ // The Node runtime loads the .func as CommonJS by default, so mark the whole
235
+ // function dir as ESM. (The edge runtime always treats sources as ESM.)
236
+ if (!isEdge) {
237
+ await writeFile(join(fnDir, "package.json"), JSON.stringify({ type: "module" }, null, 2) + "\n");
238
+ }
203
239
 
204
240
  // static: only the hashed framework assets under /_june/ (exact-path URLs)
205
241
  if (hasAssets) {
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
@@ -16,6 +16,8 @@ import type { DbFactory } from "@junejs/core/resources";
16
16
  import { host } from "./host";
17
17
 
18
18
  export { d1, type D1Database } from "./d1";
19
+ export { postgres, pgJuneDb, type PgClientLike } from "./postgres-driver";
20
+ export { mysql, mysqlJuneDb, type MysqlConnLike } from "./mysql-driver";
19
21
 
20
22
  // Local SQLite — the zero-config dev default. A persistent FILE, not :memory:,
21
23
  // on purpose: `june dev` restarts the process on every save (the watch
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, type D1Database } from "./db";
15
15
  export { memoryKv, redisKv } from "./kv";
16
16
  export { localBlob, r2, type R2Bucket } from "./blob";
17
17
  export {
@@ -29,6 +29,7 @@ export { loadJuneConfig } from "./config-loader";
29
29
  export {
30
30
  migrate,
31
31
  migrateApp,
32
+ typesApp,
32
33
  classify,
33
34
  readMigrations,
34
35
  blockedMessage,
package/src/migrate.ts CHANGED
@@ -112,6 +112,24 @@ export async function migrateApp(
112
112
  }
113
113
  }
114
114
 
115
+ // `june db types`: open the declared db, bring it to head (safe migrations only, so
116
+ // the schema is present even on a fresh checkout), then hand it to the data layer's
117
+ // codegen hook for the type-declaration text. Returns null when there's no db or the
118
+ // data layer doesn't generate types — the CLI owns the messaging. Introspection is
119
+ // read-only; the safe migrate is idempotent (the ledger skips applied ones).
120
+ export async function typesApp(root: string, config: JuneConfig): Promise<string | null> {
121
+ const factory = config.resources?.db;
122
+ const emit = config.dataLayer?.emitTypes;
123
+ if (!factory || !emit) return null;
124
+ const db = await factory.open();
125
+ try {
126
+ await migrate(db, join(root, "db", "migrations"), { allowDestructive: false });
127
+ return await emit(db);
128
+ } finally {
129
+ await db.close();
130
+ }
131
+ }
132
+
115
133
  // The "this migration was gated" guidance, shared by dev (warns, keeps serving)
116
134
  // and `june db migrate` (exits non-zero).
117
135
  export function blockedMessage(m: ClassifiedMigration): string {
@@ -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
+ }
@@ -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
  },