@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 +3 -3
- package/src/adapter.ts +66 -0
- package/src/build.ts +3 -2
- package/src/css.ts +41 -11
- package/src/deploy.ts +70 -4
- package/src/host.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kv.ts +1 -1
- package/src/worker.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
34
|
-
"@junejs/db": "0.0.
|
|
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 =
|
|
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 —
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
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
|
|
116
|
-
//
|
|
117
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
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
|
+
}
|