@junejs/server 0.0.19 → 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.19",
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.19",
34
- "@junejs/db": "0.0.19",
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/build.ts CHANGED
@@ -34,7 +34,7 @@ import { createWorker, type WorkerManifest } from "./worker";
34
34
  import { findExtraFile } from "./router";
35
35
  import type { ExtraHandler, LayoutComponent, LoadingComponent } from "./pipeline";
36
36
  import { findClientEntry, bundleClientToFile, CLIENT_SCRIPT_URL } from "./client-bundle";
37
- import { findGlobalCss, minifyCss, processCss, STYLES_URL } from "./css";
37
+ import { cssTargets, findGlobalCss, minifyCss, processCss, STYLES_URL } from "./css";
38
38
  import { buildModuleCss, rolldownCssModulesPlugin, registerCssModules } from "./css-modules";
39
39
 
40
40
  export type BuildResult = {
@@ -290,7 +290,8 @@ export async function juneBuild(
290
290
  const { maps: cssModuleMaps, css: rawModuleCss } = await buildModuleCss(appDir, appRoot);
291
291
  // Minify for build (dev serves it readable). Scoped class names are untouched,
292
292
  // so the hashed sheet still matches the maps the bundlers/loaders hand out.
293
- const moduleCss = rawModuleCss === null ? null : await minifyCss(rawModuleCss, "modules.css");
293
+ const moduleCss =
294
+ rawModuleCss === null ? null : await minifyCss(rawModuleCss, "modules.css", await cssTargets(appDir));
294
295
  let moduleCssAsset: string | null = null;
295
296
  if (moduleCss !== null) {
296
297
  const hash = createHash("sha256").update(moduleCss).digest("hex").slice(0, 8);
package/src/css.ts CHANGED
@@ -15,6 +15,8 @@ import { createRequire } from "node:module";
15
15
  import { join } from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
 
18
+ import type { Targets } from "lightningcss"; // type-only — never enters the graph
19
+
18
20
  // Framework-emitted assets live under the reserved /_june/ prefix so they never
19
21
  // collide with — or squat on — a user's own paths (cf. Next /_next/, Astro
20
22
  // /_astro/). Build content-hashes the file under here; this is the dev URL.
@@ -39,6 +41,32 @@ function resolveFromApp(specifier: string, appDir: string): string | null {
39
41
  }
40
42
  }
41
43
 
44
+ // Browser targets for autoprefix + syntax lowering (CSS nesting, :is(), oklch,
45
+ // logical properties, …). June adds NO dep here: if the app installs `browserslist`
46
+ // its config (or the browserslist defaults) drives the targets; otherwise a baked,
47
+ // broadly-supported baseline. Override per app the standard way — a `browserslist`
48
+ // field in package.json or a .browserslistrc.
49
+ const v = (major: number, minor = 0): number => (major << 16) | (minor << 8);
50
+ const DEFAULT_TARGETS: Targets = { chrome: v(107), edge: v(107), firefox: v(104), safari: v(16) };
51
+
52
+ let targetsCache: { appDir: string; targets: Targets } | null = null;
53
+ export async function cssTargets(appDir: string): Promise<Targets> {
54
+ if (targetsCache?.appDir === appDir) return targetsCache.targets;
55
+ let targets: Targets = DEFAULT_TARGETS;
56
+ const blPath = resolveFromApp("browserslist", appDir);
57
+ if (blPath) {
58
+ try {
59
+ const browserslist = ((await import(blPath)) as { default: (q?: unknown, o?: unknown) => string[] }).default;
60
+ const { browserslistToTargets } = await import("lightningcss");
61
+ targets = browserslistToTargets(browserslist(undefined, { path: appDir }));
62
+ } catch {
63
+ /* malformed config / resolution race → keep the baked default */
64
+ }
65
+ }
66
+ targetsCache = { appDir, targets };
67
+ return targets;
68
+ }
69
+
42
70
  async function compileTailwind(
43
71
  css: string,
44
72
  file: string,
@@ -76,16 +104,17 @@ export function invalidateCss(): void {
76
104
  devCache = null;
77
105
  }
78
106
 
79
- // Minify a finished stylesheet with Lightning CSS — the same engine Tailwind v4
80
- // optimizes with, so all of June's build-time CSS comes out consistently. It only
81
- // touches whitespace/comments/redundancy, never identifiers, so scoped CSS-module
82
- // class names survive intact (hydration stays byte-identical to SSR). Lazy import:
83
- // build/dev-time only, never in the worker graph. Falls back to the input if
84
- // Lightning CSS can't parse it, so minification can never break a build.
85
- export async function minifyCss(css: string, filename = "input.css"): Promise<string> {
107
+ // Minify a finished stylesheet with Lightning CSS — and, given `targets`, ALSO
108
+ // autoprefix + lower modern syntax (nesting, :is(), oklch, logical props) for the
109
+ // target browsers. The same engine Tailwind v4 optimizes with, so all of June's
110
+ // build-time CSS comes out consistently. It never touches identifiers, so scoped
111
+ // CSS-module class names survive intact (hydration stays byte-identical to SSR).
112
+ // Lazy import: build/dev-time only, never in the worker graph. Falls back to the
113
+ // input if Lightning CSS can't parse it, so it can never break a build.
114
+ export async function minifyCss(css: string, filename = "input.css", targets?: Targets): Promise<string> {
86
115
  try {
87
116
  const { transform } = await import("lightningcss");
88
- const { code } = transform({ filename, code: Buffer.from(css), minify: true });
117
+ const { code } = transform({ filename, code: Buffer.from(css), minify: true, targets });
89
118
  return code.toString();
90
119
  } catch (e) {
91
120
  console.warn("[june] CSS minify failed; emitting unminified:", e);
@@ -112,7 +141,8 @@ export async function processCss(
112
141
  "[june] global.css uses Tailwind but @tailwindcss/postcss isn't installed — serving raw CSS.",
113
142
  );
114
143
  }
115
- // Plain CSS (or the Tailwind fallback): minify here so the non-Tailwind path
116
- // is optimized too, matching the Tailwind path.
117
- return opts.minify ? await minifyCss(css, file) : css;
144
+ // Plain CSS (or the Tailwind fallback): minify + autoprefix/lower for the app's
145
+ // browser targets, so the non-Tailwind path is optimized too. (Tailwind v4 owns
146
+ // its own targeting; dev stays readable and untargeted.)
147
+ return opts.minify ? await minifyCss(css, file, await cssTargets(appDir)) : css;
118
148
  }
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
+ }