bosia 0.4.4 → 0.5.0
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 +1 -1
- package/src/cli/add.ts +4 -2
- package/src/cli/block.ts +94 -0
- package/src/cli/fonts.ts +61 -0
- package/src/cli/index.ts +19 -6
- package/src/cli/theme.ts +88 -0
- package/src/core/client/App.svelte +121 -5
- package/src/core/client/appState.svelte.ts +24 -37
- package/src/core/client/enhance.ts +6 -2
- package/src/core/client/hydrate.ts +51 -3
- package/src/core/client/loaderCache.ts +127 -0
- package/src/core/client/navigation.ts +59 -0
- package/src/core/client/prefetch.ts +48 -3
- package/src/core/cors.ts +57 -11
- package/src/core/csp.ts +47 -0
- package/src/core/csrf.ts +8 -5
- package/src/core/dev.ts +14 -2
- package/src/core/errors.ts +4 -3
- package/src/core/hooks.ts +37 -1
- package/src/core/html.ts +68 -26
- package/src/core/prerender.ts +11 -0
- package/src/core/renderer.ts +346 -35
- package/src/core/routeFile.ts +26 -0
- package/src/core/safePath.ts +14 -0
- package/src/core/server.ts +103 -15
- package/src/lib/client.ts +1 -0
- package/src/lib/index.ts +1 -0
package/src/core/html.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { getDeclaredEnvKeys } from "./env.ts";
|
|
3
|
+
import { nonceAttr } from "./csp.ts";
|
|
3
4
|
|
|
4
5
|
// ─── Dist Manifest ───────────────────────────────────────
|
|
5
6
|
// Maps hashed filenames → script/link tags.
|
|
@@ -36,6 +37,22 @@ export function safeJsonStringify(data: unknown): string {
|
|
|
36
37
|
return json.replace(/[<>&\u2028\u2029]/g, (c) => map[c]);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
const SCRIPT_HAZARD_RE = /<(\/script|!--)/gi;
|
|
41
|
+
|
|
42
|
+
/** Escapes JSON for safe embedding inside <script type="application/json"> blocks.
|
|
43
|
+
* Blocks premature </script> and <!-- (HTML script-data escape state) without
|
|
44
|
+
* the JS-context overhead of safeJsonStringify. */
|
|
45
|
+
export function safeJsonForScript(data: unknown): string {
|
|
46
|
+
let json: string;
|
|
47
|
+
try {
|
|
48
|
+
json = JSON.stringify(data);
|
|
49
|
+
} catch {
|
|
50
|
+
console.error("safeJsonForScript: failed to serialize data (circular reference?)");
|
|
51
|
+
json = "null";
|
|
52
|
+
}
|
|
53
|
+
return json.replace(SCRIPT_HAZARD_RE, "\\u003c$1");
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
// ─── Public Env Injection ─────────────────────────────────
|
|
40
57
|
|
|
41
58
|
/**
|
|
@@ -76,6 +93,9 @@ export function buildHtml(
|
|
|
76
93
|
formData: any = null,
|
|
77
94
|
lang?: string,
|
|
78
95
|
ssr = true,
|
|
96
|
+
nonce?: string,
|
|
97
|
+
pageDeps: any = null,
|
|
98
|
+
layoutDeps: any[] | null = null,
|
|
79
99
|
): string {
|
|
80
100
|
const cssLinks = (distManifest.css ?? [])
|
|
81
101
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
@@ -83,20 +103,35 @@ export function buildHtml(
|
|
|
83
103
|
|
|
84
104
|
const fallbackTitle = head.includes("<title>") ? "" : "<title>Bosia App</title>";
|
|
85
105
|
|
|
106
|
+
const n = nonceAttr(nonce);
|
|
86
107
|
const publicEnv = getPublicDynamicEnv();
|
|
87
108
|
const envScript =
|
|
88
109
|
Object.keys(publicEnv).length > 0
|
|
89
|
-
? `\n <script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
|
|
110
|
+
? `\n <script${n}>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`
|
|
90
111
|
: "";
|
|
91
112
|
|
|
92
|
-
const formScript =
|
|
93
|
-
formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
|
|
94
113
|
const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
|
|
95
114
|
|
|
115
|
+
const depsScript =
|
|
116
|
+
pageDeps !== null || layoutDeps !== null
|
|
117
|
+
? `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`
|
|
118
|
+
: "";
|
|
119
|
+
|
|
120
|
+
const sysScript =
|
|
121
|
+
ssrFlag || depsScript ? `\n <script${n}>${ssrFlag}${depsScript}</script>` : "";
|
|
122
|
+
|
|
123
|
+
const dataIslands = csr
|
|
124
|
+
? `\n <script${n} type="application/json" id="__bosia-page-data__">${safeJsonForScript(pageData)}</script>` +
|
|
125
|
+
`\n <script${n} type="application/json" id="__bosia-layout-data__">${safeJsonForScript(layoutData)}</script>` +
|
|
126
|
+
(formData != null
|
|
127
|
+
? `\n <script${n} type="application/json" id="__bosia-form-data__">${safeJsonForScript(formData)}</script>`
|
|
128
|
+
: "")
|
|
129
|
+
: "";
|
|
130
|
+
|
|
96
131
|
const scripts = csr
|
|
97
|
-
? `${envScript}
|
|
132
|
+
? `${envScript}${dataIslands}${sysScript}\n <script${n} type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`
|
|
98
133
|
: isDev
|
|
99
|
-
? `\n <script>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`
|
|
134
|
+
? `\n <script${n}>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`
|
|
100
135
|
: "";
|
|
101
136
|
|
|
102
137
|
return `<!DOCTYPE html>
|
|
@@ -109,7 +144,7 @@ export function buildHtml(
|
|
|
109
144
|
${head}
|
|
110
145
|
${cssLinks}
|
|
111
146
|
<link rel="stylesheet" href="/bosia-tw.css${cacheBust}">
|
|
112
|
-
<script>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>
|
|
147
|
+
<script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>
|
|
113
148
|
</head>
|
|
114
149
|
<body>
|
|
115
150
|
<div id="app">${body}</div>${scripts}
|
|
@@ -121,27 +156,23 @@ export function buildHtml(
|
|
|
121
156
|
|
|
122
157
|
import type { Metadata } from "./hooks.ts";
|
|
123
158
|
|
|
124
|
-
const _shellOpenCache = new Map<string, string>();
|
|
125
|
-
|
|
126
159
|
/** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
|
|
127
|
-
export function buildHtmlShellOpen(lang?: string): string {
|
|
160
|
+
export function buildHtmlShellOpen(lang?: string, nonce?: string): string {
|
|
128
161
|
const key = safeLang(lang);
|
|
129
|
-
const
|
|
130
|
-
if (cached) return cached;
|
|
162
|
+
const n = nonceAttr(nonce);
|
|
131
163
|
const cssLinks = (distManifest.css ?? [])
|
|
132
164
|
.map((f: string) => `<link rel="stylesheet" href="/dist/client/${f}">`)
|
|
133
165
|
.join("\n ");
|
|
134
|
-
|
|
166
|
+
return (
|
|
135
167
|
`<!DOCTYPE html>\n<html lang="${key}">\n<head>\n` +
|
|
136
168
|
` <meta charset="UTF-8">\n` +
|
|
137
169
|
` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n` +
|
|
138
170
|
` <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n` +
|
|
139
171
|
` ${cssLinks}\n` +
|
|
140
172
|
` <link rel="stylesheet" href="/bosia-tw.css${cacheBust}">\n` +
|
|
141
|
-
` <script>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
142
|
-
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}"
|
|
143
|
-
|
|
144
|
-
return result;
|
|
173
|
+
` <script${n}>try{var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark');else document.documentElement.classList.remove('dark')}catch(_){}</script>\n` +
|
|
174
|
+
` <link rel="modulepreload" href="/dist/client/${distManifest.entry}${cacheBust}">`
|
|
175
|
+
);
|
|
145
176
|
}
|
|
146
177
|
|
|
147
178
|
const SPINNER =
|
|
@@ -208,25 +239,36 @@ export function buildHtmlTail(
|
|
|
208
239
|
formData: any = null,
|
|
209
240
|
ssr = true,
|
|
210
241
|
bodyEndExtras?: string[],
|
|
242
|
+
nonce?: string,
|
|
243
|
+
pageDeps: any = null,
|
|
244
|
+
layoutDeps: any[] | null = null,
|
|
211
245
|
): string {
|
|
212
|
-
|
|
246
|
+
const n = nonceAttr(nonce);
|
|
247
|
+
let out = `<script${n}>document.getElementById('__bs__').remove()</script>`;
|
|
213
248
|
out += `\n<div id="app">${body}</div>`;
|
|
214
249
|
if (head)
|
|
215
|
-
out += `\n<script>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
|
|
250
|
+
out += `\n<script${n}>document.head.insertAdjacentHTML('beforeend',${safeJsonStringify(head)})</script>`;
|
|
216
251
|
if (csr) {
|
|
217
252
|
const publicEnv = getPublicDynamicEnv();
|
|
218
253
|
if (Object.keys(publicEnv).length > 0) {
|
|
219
|
-
out += `\n<script>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`;
|
|
254
|
+
out += `\n<script${n}>window.__BOSIA_ENV__=${safeJsonStringify(publicEnv)};</script>`;
|
|
255
|
+
}
|
|
256
|
+
out += `\n<script${n} type="application/json" id="__bosia-page-data__">${safeJsonForScript(pageData)}</script>`;
|
|
257
|
+
out += `\n<script${n} type="application/json" id="__bosia-layout-data__">${safeJsonForScript(layoutData)}</script>`;
|
|
258
|
+
if (formData != null) {
|
|
259
|
+
out += `\n<script${n} type="application/json" id="__bosia-form-data__">${safeJsonForScript(formData)}</script>`;
|
|
220
260
|
}
|
|
221
|
-
const formInject =
|
|
222
|
-
formData != null ? `window.__BOSIA_FORM_DATA__=${safeJsonStringify(formData)};` : "";
|
|
223
261
|
const ssrFlag = ssr ? "" : "window.__BOSIA_SSR__=false;";
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
const depsInject =
|
|
263
|
+
pageDeps !== null || layoutDeps !== null
|
|
264
|
+
? `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`
|
|
265
|
+
: "";
|
|
266
|
+
if (ssrFlag || depsInject) {
|
|
267
|
+
out += `\n<script${n}>${ssrFlag}${depsInject}</script>`;
|
|
268
|
+
}
|
|
269
|
+
out += `\n<script${n} type="module" src="/dist/client/${distManifest.entry}${cacheBust}"></script>`;
|
|
228
270
|
} else if (isDev) {
|
|
229
|
-
out += `\n<script>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
|
|
271
|
+
out += `\n<script${n}>!function r(){var e=new EventSource("/__bosia/sse");e.addEventListener("reload",()=>location.reload());e.onopen=()=>r._ok||(r._ok=1);e.onerror=()=>{e.close();setTimeout(r,2000)}}()</script>`;
|
|
230
272
|
}
|
|
231
273
|
if (bodyEndExtras?.length) {
|
|
232
274
|
for (const fragment of bodyEndExtras) {
|
package/src/core/prerender.ts
CHANGED
|
@@ -44,6 +44,17 @@ interface PrerenderTarget {
|
|
|
44
44
|
export function substituteParams(pattern: string, entry: Record<string, string>): string {
|
|
45
45
|
let resolved = pattern;
|
|
46
46
|
for (const [key, value] of Object.entries(entry)) {
|
|
47
|
+
// `..` and `\` are never legitimate in a route segment — they let a build
|
|
48
|
+
// emit prerendered HTML outside the intended output tree. Forward slashes
|
|
49
|
+
// are only allowed for catch-all (`[...key]`) segments, which by design
|
|
50
|
+
// expand to multiple path parts. Validate accordingly.
|
|
51
|
+
const isRest = pattern.includes(`[...${key}]`);
|
|
52
|
+
if (/\\|\.\./.test(value) || (!isRest && value.includes("/"))) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Prerender entries(): unsafe value "${value}" for [${key}] — ` +
|
|
55
|
+
`path traversal characters are not allowed in dynamic segment values.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
47
58
|
resolved = resolved.replace(`[...${key}]`, value);
|
|
48
59
|
resolved = resolved.replace(`[${key}]`, value);
|
|
49
60
|
}
|