@junejs/server 0.0.20 → 0.0.21

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.20",
3
+ "version": "0.0.21",
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.20",
34
- "@junejs/db": "0.0.20",
33
+ "@junejs/core": "0.0.21",
34
+ "@junejs/db": "0.0.21",
35
35
  "marked": "^18.0.5",
36
36
  "rolldown": "^1.1.0",
37
37
  "lightningcss": "^1.27.0"
package/src/adapter.ts CHANGED
@@ -268,3 +268,69 @@ export function vercel(opts?: { runtime?: "node" | "edge"; regions?: string[] })
268
268
  },
269
269
  };
270
270
  }
271
+
272
+ // Deno Deploy target. The SAME portable bundle as workers/vercel (edge-light →
273
+ // react-dom server.edge.js, which Deno's web-standard runtime runs); the entry
274
+ // wraps the pipeline in `Deno.serve(withDenoAssets(...))`. Deno Deploy has no CDN,
275
+ // so the handler serves /_june/* from the co-located assets/ dir itself (like
276
+ // vercel()'s split, but in-process). `june build` produces dist/{worker.js,assets}
277
+ // and `deployctl deploy` ships the dir — no Build Output tree to construct.
278
+ //
279
+ // db: turso() works (libsql over HTTPS, env-driven). sqlite()/d1() are
280
+ // Cloudflare-only; validate() rejects them.
281
+ export function deno(opts?: { org?: string; app?: string }): JuneAdapter {
282
+ return {
283
+ name: "deno",
284
+ capabilities: { runtime: "edge", persistentConnections: true, assets: "server" },
285
+ // "deno" first (a dep may ship a Deno-specific build); else edge-light →
286
+ // server.edge.js (Web Streams + fetch, which Deno has).
287
+ conditions: ["deno", "edge-light", "edge", "import", "default"],
288
+
289
+ validate({ config }) {
290
+ const kind = config.resources?.db?.kind;
291
+ if (kind && kind !== "turso") {
292
+ throw new Error(
293
+ `deno(): the '${kind}' db isn't supported on Deno Deploy (D1 is Cloudflare-only).\n` +
294
+ " Use turso() (libsql over HTTPS) for a Deno db, or deploy to Workers.",
295
+ );
296
+ }
297
+ },
298
+
299
+ entry() {
300
+ // The `export default { fetch }` Web Standard export — what `deno serve` (and
301
+ // Deno Deploy's runtime) invokes, and the same shape vercel()'s Node runtime
302
+ // uses. (A top-level `Deno.serve()` only works under `deno run`, not the
303
+ // platform.) No env glue: withDenoAssets reads Deno.env itself.
304
+ return {
305
+ imports: [`import { withDenoAssets } from "@junejs/server/worker";`],
306
+ wrap: (pipelineVar) => `export default { fetch: withDenoAssets(${pipelineVar}) };`,
307
+ };
308
+ },
309
+
310
+ async emit({ outDir }) {
311
+ // The bundle (worker.js + chunks) and assets/ already sit in outDir. The
312
+ // deno.json declares the Deno Deploy target (org/app) + the runtime block.
313
+ // `type: "dynamic"` is REQUIRED — it's the tag that tells EA "run this
314
+ // entrypoint as a server" (the `{ fetch }` export `deno serve` runs). Omit
315
+ // it and EA falls back to framework auto-detection ("internally optimized"),
316
+ // finds nothing, and the revision dies with "No runtime entrypoint provided".
317
+ // (The other arm is `type: "static"` + cwd/spa for a prebuilt SPA.) Source
318
+ // config takes precedence over the dashboard, so the deploy is reproducible.
319
+ // org/app come from deno({ org, app }) — without them, `deno deploy` prompts.
320
+ await writeFile(
321
+ join(outDir, "deno.json"),
322
+ JSON.stringify(
323
+ {
324
+ deploy: {
325
+ ...(opts?.org ? { org: opts.org } : {}),
326
+ ...(opts?.app ? { app: opts.app } : {}),
327
+ runtime: { type: "dynamic", entrypoint: "worker.js" },
328
+ },
329
+ },
330
+ null,
331
+ 2,
332
+ ) + "\n",
333
+ );
334
+ },
335
+ };
336
+ }
package/src/deploy.ts CHANGED
@@ -52,7 +52,7 @@ export async function juneDeploy(appRoot: string, options: DeployOptions = {}):
52
52
  // fallback. workers() is the default when neither names one.
53
53
  const adapterName = (cfg.deploy?.adapter as { name?: string } | undefined)?.name;
54
54
  const target = adapterName ?? cfg.deploy?.target ?? "workers";
55
- if (target !== "workers" && target !== "vercel") {
55
+ if (target !== "workers" && target !== "vercel" && target !== "deno") {
56
56
  throw new Error(`unknown deploy target: ${target}`);
57
57
  }
58
58
 
@@ -62,9 +62,75 @@ export async function juneDeploy(appRoot: string, options: DeployOptions = {}):
62
62
  }
63
63
 
64
64
  const run = options.runCli ?? spawnCli;
65
- return target === "vercel"
66
- ? deployVercel(appRoot, options, run)
67
- : deployWorkers(appRoot, cfg, options, run);
65
+ if (target === "vercel") return deployVercel(appRoot, options, run);
66
+ if (target === "deno") return deployDeno(appRoot, cfg, options, run);
67
+ return deployWorkers(appRoot, cfg, options, run);
68
+ }
69
+
70
+ // --- Deno Deploy: `deno deploy` ships the built dir (worker.js + assets/) -------
71
+ // ONE-TIME app provisioning (the trap): the app must be created in DYNAMIC mode
72
+ // with worker.js as the entrypoint, or Deno Deploy auto-detects a framework
73
+ // ("internally optimized"), finds none, and every build dies with "No runtime
74
+ // entrypoint provided". The emitted deno.json's deploy.runtime.type="dynamic"
75
+ // only takes precedence AFTER a successful source-config build — a chicken-and-egg
76
+ // the first build can't satisfy. So provision once with the config forced:
77
+ // deno deploy create --org=<org> --app=<app> --source=local \
78
+ // --do-not-use-detected-build-config --runtime-mode=dynamic --entrypoint=worker.js
79
+ // After that, plain `deno deploy` re-uses the stuck-free config on every ship.
80
+ const DENO_CREATE_HINT =
81
+ "first deploy? provision the app in dynamic mode (auto-detect won't find an entrypoint):\n" +
82
+ " deno deploy create --org=<org> --app=<app> --source=local \\\n" +
83
+ " --do-not-use-detected-build-config --runtime-mode=dynamic --entrypoint=worker.js";
84
+
85
+ async function deployDeno(
86
+ appRoot: string,
87
+ cfg: Awaited<ReturnType<typeof loadJuneConfig>>,
88
+ options: DeployOptions,
89
+ run: CliRun,
90
+ ): Promise<DeployResult> {
91
+ const dist = join(appRoot, "dist");
92
+ if (!existsSync(join(dist, "worker.js"))) {
93
+ throw new Error(`no built bundle (expected ${join(dist, "worker.js")}) — run june build first`);
94
+ }
95
+ if (options.dryRun) {
96
+ console.log(`dry-run: built ${dist} (skipping deno deploy)`);
97
+ return { url: null, dryRun: true, configPath: dist, migrated: [] };
98
+ }
99
+ if (!process.env.DENO_DEPLOY_TOKEN) {
100
+ console.log(
101
+ "note: DENO_DEPLOY_TOKEN not set — `deno deploy` will use its own login\n" +
102
+ " (run `deno deploy login` once, or export the token).",
103
+ );
104
+ }
105
+ // Deno Deploy (the current platform; Classic + deployctl retire 2026-07-20). The
106
+ // emitted deno.json carries deploy.{org,app,runtime}, so `deno deploy` ships dist/
107
+ // (worker.js + chunks + assets/) with no extra flags onto an already-provisioned
108
+ // app. --prod targets production (default is a preview). Auth from DENO_DEPLOY_TOKEN.
109
+ const args = ["deno", "deploy"];
110
+ if (options.prod) args.push("--prod");
111
+
112
+ const { stdout, stderr, exitCode } = await run(args, dist);
113
+ if (stdout.trim()) console.log(stdout.trimEnd());
114
+ if (exitCode !== 0) {
115
+ const out = stderr + stdout;
116
+ if (/token|login|unauthorized|forbidden|organization was not found|app.*not found/i.test(out)) {
117
+ throw new Error(
118
+ "Deno Deploy auth/org failed — set DENO_DEPLOY_TOKEN (or `deno deploy login`) and the\n" +
119
+ " org/app in deno({ org, app }).\n " +
120
+ DENO_CREATE_HINT +
121
+ "\n" +
122
+ stderr.trim(),
123
+ );
124
+ }
125
+ if (/no runtime entrypoint|build config/i.test(out)) {
126
+ throw new Error("Deno Deploy build failed — " + DENO_CREATE_HINT + "\n" + stderr.trim());
127
+ }
128
+ throw new Error(`deno deploy failed (exit ${exitCode})\n${stderr.trim()}`);
129
+ }
130
+ // EA serves on *.deno.net (the prod alias <app>.<org>.deno.net + the per-revision
131
+ // preview); Classic used *.deno.dev. Match both so the URL is always surfaced.
132
+ const url = stdout.match(/https:\/\/\S+\.deno\.(net|dev)\S*/)?.[0] ?? null;
133
+ return { url, dryRun: false, configPath: dist, migrated: [] };
68
134
  }
69
135
 
70
136
  // --- Vercel: upload the prebuilt Build Output (.vercel/output/) ---------------
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.md: openDb is demoted from the
52
+ // adapter builds on (docs/data-layer-boundary.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
@@ -4,6 +4,7 @@
4
4
  export {
5
5
  workers,
6
6
  vercel,
7
+ deno,
7
8
  type JuneAdapter,
8
9
  type AdapterCapabilities,
9
10
  type ResourcePlan,
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-boundary
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/worker.ts CHANGED
@@ -215,3 +215,54 @@ export function withAssets(
215
215
  },
216
216
  };
217
217
  }
218
+
219
+ // The Deno-target twin of withAssets. Deno Deploy has NO CDN / asset binding, so
220
+ // the handler serves the hashed framework assets (/_june/*) from the bundle's
221
+ // co-located assets/ dir and falls through to the pipeline for everything else —
222
+ // the same split vercel() gets from the CDN, but in-process. The deno() adapter
223
+ // bakes `export default { fetch: withDenoAssets(pipeline) }` into the entry (the
224
+ // Web Standard handler `deno serve` / Deno Deploy runs). Deno globals are
225
+ // referenced only inside the body, so non-Deno bundles tree-shake this out.
226
+ declare const Deno: {
227
+ readFile(path: string | URL): Promise<Uint8Array>;
228
+ };
229
+
230
+ const ASSET_CONTENT_TYPES: Record<string, string> = {
231
+ css: "text/css; charset=utf-8",
232
+ js: "text/javascript; charset=utf-8",
233
+ json: "application/json; charset=utf-8",
234
+ map: "application/json; charset=utf-8",
235
+ svg: "image/svg+xml",
236
+ png: "image/png",
237
+ jpg: "image/jpeg",
238
+ jpeg: "image/jpeg",
239
+ webp: "image/webp",
240
+ avif: "image/avif",
241
+ ico: "image/x-icon",
242
+ woff2: "font/woff2",
243
+ woff: "font/woff",
244
+ };
245
+
246
+ export function withDenoAssets(pipeline: FetchPipeline): (request: Request) => Promise<Response> {
247
+ return async (request) => {
248
+ const url = new URL(request.url);
249
+ if (request.method === "GET" && url.pathname.startsWith("/_june/")) {
250
+ try {
251
+ const file = await Deno.readFile(new URL(`./assets${url.pathname}`, import.meta.url));
252
+ const ext = url.pathname.split(".").pop()?.toLowerCase() ?? "";
253
+ return new Response(file as BodyInit, {
254
+ headers: {
255
+ "content-type": ASSET_CONTENT_TYPES[ext] ?? "application/octet-stream",
256
+ "cache-control": "public, max-age=31536000, immutable",
257
+ },
258
+ });
259
+ } catch {
260
+ /* not on disk → fall through (the pipeline 404s) */
261
+ }
262
+ }
263
+ // No env arg: Deno has no platform bindings (unlike D1/KV on workers); env-
264
+ // driven resources (turso) read process.env / Deno.env themselves. Keeps the
265
+ // function off the --allow-env permission too.
266
+ return pipeline.fetch(request);
267
+ };
268
+ }