@junejs/server 0.0.7 → 0.0.8
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 +2 -2
- package/src/build.ts +10 -1
- package/src/db.ts +17 -4
- package/src/pipeline.ts +26 -0
- package/src/router.ts +19 -6
- package/src/worker.ts +45 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@junejs/core": "0.0.
|
|
32
|
+
"@junejs/core": "0.0.8",
|
|
33
33
|
"marked": "^18.0.5",
|
|
34
34
|
"rolldown": "^1.1.0"
|
|
35
35
|
},
|
package/src/build.ts
CHANGED
|
@@ -341,15 +341,24 @@ ${chains.join("\n")}
|
|
|
341
341
|
const pkg = existsSync(pkgPath)
|
|
342
342
|
? (JSON.parse(await Bun.file(pkgPath).text()) as { name?: string })
|
|
343
343
|
: {};
|
|
344
|
+
const deployCfg = (await loadJuneConfig(appRoot)).deploy;
|
|
344
345
|
await writeFile(
|
|
345
346
|
join(outDir, "wrangler.jsonc"),
|
|
346
347
|
JSON.stringify(
|
|
347
348
|
{
|
|
348
|
-
name:
|
|
349
|
+
name:
|
|
350
|
+
deployCfg?.name ??
|
|
351
|
+
(pkg.name ?? basename(appRoot)).replace(/[^a-z0-9-]/gi, "-").toLowerCase(),
|
|
349
352
|
main: "./worker.js",
|
|
350
353
|
compatibility_date: "2025-01-01",
|
|
351
354
|
compatibility_flags: ["nodejs_compat"],
|
|
352
355
|
...(hasAssets ? { assets: { directory: "./assets" } } : {}),
|
|
356
|
+
// config deploy.domain → a Workers custom domain; without it the
|
|
357
|
+
// generated file regenerating on every build would silently drop a
|
|
358
|
+
// hand-attached domain.
|
|
359
|
+
...(deployCfg?.domain
|
|
360
|
+
? { routes: [{ pattern: deployCfg.domain, custom_domain: true }] }
|
|
361
|
+
: {}),
|
|
353
362
|
},
|
|
354
363
|
null,
|
|
355
364
|
2,
|
package/src/db.ts
CHANGED
|
@@ -3,15 +3,28 @@
|
|
|
3
3
|
// by the host, injected onto RouteContext as `ctx.db`. The framework depends on
|
|
4
4
|
// the JuneDb contract; these adapters (and Juno on top) are swappable.
|
|
5
5
|
|
|
6
|
+
import { mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
|
|
6
9
|
import type { DbFactory, JuneDb, RunResult } from "@junejs/core/resources";
|
|
7
10
|
|
|
8
11
|
import { host } from "./host";
|
|
9
12
|
|
|
10
|
-
// Local SQLite — the zero-config dev default
|
|
11
|
-
// on
|
|
13
|
+
// Local SQLite — the zero-config dev default. A persistent FILE, not :memory:,
|
|
14
|
+
// on purpose: `june dev` restarts the process on every save (the watch
|
|
15
|
+
// supervisor), so an in-memory default would evaporate your dev data at each
|
|
16
|
+
// edit. A file also makes the dev db inspectable with the sqlite3 every
|
|
17
|
+
// machine already has (`sqlite3 .june/dev.sqlite`). Pass ":memory:" explicitly
|
|
18
|
+
// for ephemerality.
|
|
12
19
|
export function sqlite(opts: { path?: string } = {}): DbFactory {
|
|
13
|
-
const path = opts.path ?? "
|
|
14
|
-
return {
|
|
20
|
+
const path = opts.path ?? ".june/dev.sqlite";
|
|
21
|
+
return {
|
|
22
|
+
kind: "sqlite",
|
|
23
|
+
open: () => {
|
|
24
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
return host.openDb(path);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
// --- D1 (Cloudflare) — the third openDb impl (rebuild-plan Phase 5) ----------
|
package/src/pipeline.ts
CHANGED
|
@@ -95,6 +95,23 @@ function text(body: string, contentType: string, init?: ResponseInit) {
|
|
|
95
95
|
return new Response(body, { ...init, headers });
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// The default favicon: the site name's first character in a rounded square —
|
|
99
|
+
// a plain SVG string, so it needs no fonts, no rasterizer, and works for CJK
|
|
100
|
+
// names as readily as latin ones. Served at /favicon.svg AND /favicon.ico
|
|
101
|
+
// (browsers respect the svg content-type), so no June app 404s its icon.
|
|
102
|
+
function letterFavicon(siteName: string | undefined): Response {
|
|
103
|
+
const first = (siteName ?? "").trim().charAt(0) || "•";
|
|
104
|
+
const letter = first.toUpperCase().replace(/[<>&"']/g, "");
|
|
105
|
+
return text(
|
|
106
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">` +
|
|
107
|
+
`<rect width="64" height="64" rx="12" fill="#1d1d1f"/>` +
|
|
108
|
+
`<text x="32" y="32" dy=".36em" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif" font-size="34" font-weight="600" fill="#fbfbf8">${letter || "•"}</text>` +
|
|
109
|
+
`</svg>\n`,
|
|
110
|
+
"image/svg+xml",
|
|
111
|
+
{ headers: { "cache-control": "public, max-age=86400" } },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
export function createPipeline(cfg: PipelineConfig): Pipeline {
|
|
99
116
|
const { docConfig, agent } = cfg;
|
|
100
117
|
const NotFound = cfg.notFoundComponent ?? DefaultNotFound;
|
|
@@ -219,6 +236,15 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
|
|
|
219
236
|
if (out) return out;
|
|
220
237
|
}
|
|
221
238
|
|
|
239
|
+
// --- default favicon (after extra so an app can override it) ----------
|
|
240
|
+
if (
|
|
241
|
+
request.method === "GET" &&
|
|
242
|
+
!docConfig.site.icon &&
|
|
243
|
+
(url.pathname === "/favicon.svg" || url.pathname === "/favicon.ico")
|
|
244
|
+
) {
|
|
245
|
+
return letterFavicon(docConfig.site.name);
|
|
246
|
+
}
|
|
247
|
+
|
|
222
248
|
// --- routes ----------------------------------------------------------
|
|
223
249
|
const { target, pathname, speculative } = negotiate(url, request);
|
|
224
250
|
const resolved = await cfg.resolve(pathname);
|
package/src/router.ts
CHANGED
|
@@ -67,8 +67,12 @@ function isSpecialFile(file: string) {
|
|
|
67
67
|
|
|
68
68
|
const isRouteGroup = (name: string) => /^\(.+\)$/.test(name);
|
|
69
69
|
const isParamDir = (name: string) => /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.test(name);
|
|
70
|
+
// [[slug]] / [[...slug]] — match like their required forms, and ALSO match the
|
|
71
|
+
// segment being absent (the param is then simply missing from ctx.params).
|
|
72
|
+
const isOptionalDir = (name: string) => /^\[\[([A-Za-z_][A-Za-z0-9_]*)\]\]$/.test(name);
|
|
73
|
+
const isOptionalCatchAllDir = (name: string) => /^\[\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]\]$/.test(name);
|
|
70
74
|
const isCatchAllDir = (name: string) => /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/.test(name);
|
|
71
|
-
const paramName = (name: string) => name.replace(/^\[(\.\.\.)?|\]
|
|
75
|
+
const paramName = (name: string) => name.replace(/^\[+(\.\.\.)?|\]+$/g, "");
|
|
72
76
|
|
|
73
77
|
type DirEntry = { name: string; dir: boolean };
|
|
74
78
|
|
|
@@ -164,7 +168,16 @@ export async function matchRouteTree(
|
|
|
164
168
|
if (hit) return hit;
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
if (rest.length === 0)
|
|
171
|
+
if (rest.length === 0) {
|
|
172
|
+
// Optional segments match ABSENCE too: descend without consuming and
|
|
173
|
+
// without setting the param.
|
|
174
|
+
for (const e of entries) {
|
|
175
|
+
if (!e.dir || !(isOptionalDir(e.name) || isOptionalCatchAllDir(e.name))) continue;
|
|
176
|
+
const hit = await descend(join(dir, e.name), [], params, segments);
|
|
177
|
+
if (hit) return hit;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
168
181
|
const [head, ...tail] = rest as [string, ...string[]];
|
|
169
182
|
|
|
170
183
|
// 1) exact static dir
|
|
@@ -173,9 +186,9 @@ export async function matchRouteTree(
|
|
|
173
186
|
const hit = await descend(join(dir, head), tail, params, segments);
|
|
174
187
|
if (hit) return hit;
|
|
175
188
|
}
|
|
176
|
-
// 2) [param] dirs
|
|
189
|
+
// 2) [param] and [[param]] dirs consume one segment
|
|
177
190
|
for (const e of entries) {
|
|
178
|
-
if (!e.dir || !isParamDir(e.name)) continue;
|
|
191
|
+
if (!e.dir || !(isParamDir(e.name) || isOptionalDir(e.name))) continue;
|
|
179
192
|
const hit = await descend(
|
|
180
193
|
join(dir, e.name),
|
|
181
194
|
tail,
|
|
@@ -184,9 +197,9 @@ export async function matchRouteTree(
|
|
|
184
197
|
);
|
|
185
198
|
if (hit) return hit;
|
|
186
199
|
}
|
|
187
|
-
// 3) [...catchAll] dirs consume everything remaining
|
|
200
|
+
// 3) [...catchAll] and [[...catchAll]] dirs consume everything remaining
|
|
188
201
|
for (const e of entries) {
|
|
189
|
-
if (!e.dir || !isCatchAllDir(e.name)) continue;
|
|
202
|
+
if (!e.dir || !(isCatchAllDir(e.name) || isOptionalCatchAllDir(e.name))) continue;
|
|
190
203
|
const hit = await descend(
|
|
191
204
|
join(dir, e.name),
|
|
192
205
|
[],
|
package/src/worker.ts
CHANGED
|
@@ -41,26 +41,45 @@ export type WorkerManifest = {
|
|
|
41
41
|
|
|
42
42
|
type Compiled = { regex: RegExp; names: string[]; def: BrandedRoute; pattern: string };
|
|
43
43
|
|
|
44
|
-
// "/posts/[slug]" | "/docs/[...path]" → matcher
|
|
44
|
+
// "/posts/[slug]" | "/docs/[...path]" | "/notes/[[tag]]" → matcher. Optional
|
|
45
|
+
// segments ([[x]], [[...x]]) wrap slash + capture together so absence matches;
|
|
46
|
+
// a pattern of ONLY optional segments must also match the bare "/".
|
|
45
47
|
function compilePattern(pattern: string): { regex: RegExp; names: string[] } {
|
|
46
48
|
const names: string[] = [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
let source = "";
|
|
50
|
+
let allOptional = true;
|
|
51
|
+
for (const seg of pattern.split("/").filter(Boolean)) {
|
|
52
|
+
const oca = seg.match(/^\[\[\.\.\.(\w+)\]\]$/);
|
|
53
|
+
if (oca) {
|
|
54
|
+
names.push(oca[1]!);
|
|
55
|
+
source += "(?:/(.+))?";
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const ca = seg.match(/^\[\.\.\.(\w+)\]$/);
|
|
59
|
+
if (ca) {
|
|
60
|
+
names.push(ca[1]!);
|
|
61
|
+
source += "/(.+)";
|
|
62
|
+
allOptional = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const op = seg.match(/^\[\[(\w+)\]\]$/);
|
|
66
|
+
if (op) {
|
|
67
|
+
names.push(op[1]!);
|
|
68
|
+
source += "(?:/([^/]+))?";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const p = seg.match(/^\[(\w+)\]$/);
|
|
72
|
+
if (p) {
|
|
73
|
+
names.push(p[1]!);
|
|
74
|
+
source += "/([^/]+)";
|
|
75
|
+
allOptional = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
source += "/" + seg.replace(/[.*+?^${}()|\\]/g, "\\$&");
|
|
79
|
+
allOptional = false;
|
|
80
|
+
}
|
|
81
|
+
if (source === "") return { regex: /^\/$/, names };
|
|
82
|
+
return { regex: new RegExp(allOptional ? `^(?:${source}|/)$` : `^${source}$`), names };
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
export function createWorker(manifest: WorkerManifest): { fetch(request: Request): Promise<Response> } {
|
|
@@ -91,7 +110,14 @@ export function createWorker(manifest: WorkerManifest): { fetch(request: Request
|
|
|
91
110
|
for (const d of dynamic) {
|
|
92
111
|
const m = pathname.match(d.regex);
|
|
93
112
|
if (m) {
|
|
94
|
-
|
|
113
|
+
// Optional segments leave their capture undefined — the param is
|
|
114
|
+
// then absent, matching the dev matcher's semantics.
|
|
115
|
+
const params = Object.fromEntries(
|
|
116
|
+
d.names.flatMap((n, i) => {
|
|
117
|
+
const v = m[i + 1];
|
|
118
|
+
return v === undefined ? [] : [[n, decodeURIComponent(v)]];
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
95
121
|
return { def: d.def, params, chain: chainFor(d.pattern) };
|
|
96
122
|
}
|
|
97
123
|
}
|