bosia 0.2.3 → 0.3.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/README.md +39 -39
- package/package.json +56 -54
- package/src/ambient.d.ts +31 -0
- package/src/cli/add.ts +120 -114
- package/src/cli/build.ts +10 -10
- package/src/cli/create.ts +142 -137
- package/src/cli/dev.ts +8 -8
- package/src/cli/feat.ts +266 -258
- package/src/cli/index.ts +51 -42
- package/src/cli/registry.ts +136 -115
- package/src/cli/start.ts +17 -17
- package/src/cli/test.ts +25 -0
- package/src/core/build.ts +72 -56
- package/src/core/client/App.svelte +177 -156
- package/src/core/client/appState.svelte.ts +33 -31
- package/src/core/client/enhance.ts +83 -78
- package/src/core/client/hydrate.ts +95 -81
- package/src/core/client/prefetch.ts +101 -94
- package/src/core/client/router.svelte.ts +64 -51
- package/src/core/cookies.ts +70 -66
- package/src/core/cors.ts +44 -35
- package/src/core/csrf.ts +38 -38
- package/src/core/dedup.ts +17 -17
- package/src/core/dev.ts +165 -168
- package/src/core/env.ts +155 -148
- package/src/core/envCodegen.ts +73 -73
- package/src/core/errors.ts +48 -49
- package/src/core/hooks.ts +50 -50
- package/src/core/html.ts +184 -145
- package/src/core/matcher.ts +130 -121
- package/src/core/paths.ts +8 -10
- package/src/core/plugin.ts +113 -107
- package/src/core/prerender.ts +191 -122
- package/src/core/renderer.ts +359 -286
- package/src/core/routeFile.ts +140 -127
- package/src/core/routeTypes.ts +144 -83
- package/src/core/scanner.ts +125 -95
- package/src/core/server.ts +538 -424
- package/src/core/types.ts +25 -20
- package/src/lib/index.ts +8 -8
- package/src/lib/utils.ts +44 -30
- package/templates/default/.prettierignore +5 -0
- package/templates/default/.prettierrc.json +9 -0
- package/templates/default/README.md +5 -5
- package/templates/default/package.json +22 -18
- package/templates/default/src/app.css +80 -80
- package/templates/default/src/app.d.ts +3 -3
- package/templates/default/src/routes/+error.svelte +7 -10
- package/templates/default/src/routes/+layout.svelte +2 -2
- package/templates/default/src/routes/+page.svelte +31 -29
- package/templates/default/src/routes/about/+page.svelte +3 -3
- package/templates/default/tsconfig.json +20 -20
- package/templates/demo/.prettierignore +5 -0
- package/templates/demo/.prettierrc.json +9 -0
- package/templates/demo/README.md +9 -9
- package/templates/demo/package.json +22 -17
- package/templates/demo/src/app.css +80 -80
- package/templates/demo/src/app.d.ts +3 -3
- package/templates/demo/src/hooks.server.ts +9 -9
- package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
- package/templates/demo/src/routes/(public)/+page.svelte +96 -67
- package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
- package/templates/demo/src/routes/+error.svelte +10 -7
- package/templates/demo/src/routes/+layout.server.ts +4 -4
- package/templates/demo/src/routes/+layout.svelte +2 -2
- package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
- package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
- package/templates/demo/src/routes/api/hello/+server.ts +25 -25
- package/templates/demo/tsconfig.json +20 -20
- package/templates/todo/.prettierignore +5 -0
- package/templates/todo/.prettierrc.json +9 -0
- package/templates/todo/README.md +9 -9
- package/templates/todo/package.json +22 -17
- package/templates/todo/src/app.css +80 -80
- package/templates/todo/src/app.d.ts +7 -7
- package/templates/todo/src/hooks.server.ts +9 -9
- package/templates/todo/src/routes/+error.svelte +10 -7
- package/templates/todo/src/routes/+layout.server.ts +4 -4
- package/templates/todo/src/routes/+layout.svelte +2 -2
- package/templates/todo/src/routes/+page.svelte +44 -44
- package/templates/todo/template.json +1 -1
- package/templates/todo/tsconfig.json +20 -20
package/src/core/env.ts
CHANGED
|
@@ -4,20 +4,20 @@ import { join } from "path";
|
|
|
4
4
|
// ─── Framework-reserved vars ─────────────────────────────
|
|
5
5
|
// These are controlled by Bosia itself — users access them via process.env directly.
|
|
6
6
|
const FRAMEWORK_VARS = new Set([
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
"PORT",
|
|
8
|
+
"NODE_ENV",
|
|
9
|
+
"BODY_SIZE_LIMIT",
|
|
10
|
+
"CSRF_ALLOWED_ORIGINS",
|
|
11
|
+
"INTERNAL_HOSTS",
|
|
12
|
+
"CORS_ALLOWED_ORIGINS",
|
|
13
|
+
"CORS_ALLOWED_METHODS",
|
|
14
|
+
"CORS_ALLOWED_HEADERS",
|
|
15
|
+
"CORS_EXPOSED_HEADERS",
|
|
16
|
+
"CORS_CREDENTIALS",
|
|
17
|
+
"CORS_MAX_AGE",
|
|
18
|
+
"LOAD_TIMEOUT",
|
|
19
|
+
"METADATA_TIMEOUT",
|
|
20
|
+
"PRERENDER_TIMEOUT",
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
23
|
// ─── .env File Parser ────────────────────────────────────
|
|
@@ -27,71 +27,83 @@ const VALID_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
27
27
|
|
|
28
28
|
/** Process escape sequences in double-quoted values. */
|
|
29
29
|
function processEscapes(raw: string): string {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
return raw.replace(/\\(.)/g, (_, ch) => {
|
|
31
|
+
switch (ch) {
|
|
32
|
+
case "n":
|
|
33
|
+
return "\n";
|
|
34
|
+
case "r":
|
|
35
|
+
return "\r";
|
|
36
|
+
case "t":
|
|
37
|
+
return "\t";
|
|
38
|
+
case "\\":
|
|
39
|
+
return "\\";
|
|
40
|
+
case '"':
|
|
41
|
+
return '"';
|
|
42
|
+
default:
|
|
43
|
+
return `\\${ch}`; // preserve unknown escapes
|
|
44
|
+
}
|
|
45
|
+
});
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** Parse a .env file content into key/value pairs. Skips comments and empty lines. */
|
|
43
|
-
function parseEnvFile(content: string, filename?: string): Record<string, string> {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
49
|
+
export function parseEnvFile(content: string, filename?: string): Record<string, string> {
|
|
50
|
+
const result: Record<string, string> = {};
|
|
51
|
+
const lines = content.split("\n");
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const trimmed = lines[i].trim();
|
|
54
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
55
|
+
const eqIdx = trimmed.indexOf("=");
|
|
56
|
+
if (eqIdx === -1) continue;
|
|
57
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
58
|
+
if (!key) continue;
|
|
59
|
+
|
|
60
|
+
// Validate key is a valid identifier (required for codegen)
|
|
61
|
+
if (!VALID_ENV_NAME.test(key)) {
|
|
62
|
+
const loc = filename ? ` in ${filename}` : "";
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Invalid env variable name "${key}"${loc} (line ${i + 1}). ` +
|
|
65
|
+
`Names must start with a letter or underscore and contain only [A-Za-z0-9_].`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
70
|
+
// Strip inline comments before quote-unescaping.
|
|
71
|
+
// Quoted: find the real closing quote, verify trailing chars are whitespace+comment, truncate.
|
|
72
|
+
// Unquoted: strip \s+#... (no space before # keeps foo#bar intact).
|
|
73
|
+
if (value.startsWith('"')) {
|
|
74
|
+
let end = -1;
|
|
75
|
+
for (let j = 1; j < value.length; j++) {
|
|
76
|
+
if (value[j] === "\\") {
|
|
77
|
+
j++;
|
|
78
|
+
continue;
|
|
79
|
+
} // skip escaped char
|
|
80
|
+
if (value[j] === '"') {
|
|
81
|
+
end = j;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (end !== -1 && /^\s*(#.*)?$/.test(value.slice(end + 1))) {
|
|
86
|
+
value = value.slice(0, end + 1);
|
|
87
|
+
}
|
|
88
|
+
} else if (value.startsWith("'")) {
|
|
89
|
+
const end = value.indexOf("'", 1);
|
|
90
|
+
if (end !== -1 && /^\s*(#.*)?$/.test(value.slice(end + 1))) {
|
|
91
|
+
value = value.slice(0, end + 1);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
value = value.startsWith("#") ? "" : value.replace(/\s+#.*$/, "");
|
|
95
|
+
}
|
|
96
|
+
// Double-quoted: process escape sequences
|
|
97
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
98
|
+
value = processEscapes(value.slice(1, -1));
|
|
99
|
+
}
|
|
100
|
+
// Single-quoted: literal (no escape processing)
|
|
101
|
+
else if (value.startsWith("'") && value.endsWith("'")) {
|
|
102
|
+
value = value.slice(1, -1);
|
|
103
|
+
}
|
|
104
|
+
result[key] = value;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
// ─── Env Loader ──────────────────────────────────────────
|
|
@@ -111,51 +123,46 @@ function parseEnvFile(content: string, filename?: string): Record<string, string
|
|
|
111
123
|
* @returns merged env record (only vars from .env files, excluding framework vars)
|
|
112
124
|
*/
|
|
113
125
|
export function loadEnv(mode: string, dir?: string): Record<string, string> {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
result[key] = process.env[key] ?? value;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return result;
|
|
126
|
+
const root = dir ?? process.cwd();
|
|
127
|
+
const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
|
|
128
|
+
|
|
129
|
+
let merged: Record<string, string> = {};
|
|
130
|
+
const loaded: string[] = [];
|
|
131
|
+
|
|
132
|
+
for (const filename of files) {
|
|
133
|
+
const filepath = join(root, filename);
|
|
134
|
+
if (!existsSync(filepath)) continue;
|
|
135
|
+
const content = readFileSync(filepath, "utf-8");
|
|
136
|
+
const parsed = parseEnvFile(content, filename);
|
|
137
|
+
merged = { ...merged, ...parsed };
|
|
138
|
+
loaded.push(filename);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (loaded.length > 0) {
|
|
142
|
+
console.log(`✓ Loaded ${loaded.join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Track declared keys so html.ts only exposes .env-declared PUBLIC_* vars
|
|
146
|
+
for (const key of Object.keys(merged)) {
|
|
147
|
+
_declaredKeys.add(key);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Apply to process.env — system env wins (don't overwrite existing)
|
|
151
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
152
|
+
if (!(key in process.env)) {
|
|
153
|
+
process.env[key] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Return only non-framework vars
|
|
158
|
+
const result: Record<string, string> = {};
|
|
159
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
160
|
+
if (!FRAMEWORK_VARS.has(key)) {
|
|
161
|
+
result[key] = process.env[key] ?? value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
// ─── Declared Key Tracking ───────────────────────────
|
|
@@ -165,40 +172,40 @@ const _declaredKeys = new Set<string>();
|
|
|
165
172
|
|
|
166
173
|
/** Returns the set of env var keys that were declared in .env files. */
|
|
167
174
|
export function getDeclaredEnvKeys(): ReadonlySet<string> {
|
|
168
|
-
|
|
175
|
+
return _declaredKeys;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
// ─── Classifier ──────────────────────────────────────────
|
|
172
179
|
|
|
173
180
|
export interface ClassifiedEnv {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
/** PUBLIC_STATIC_* — client+server, build-time inlined */
|
|
182
|
+
publicStatic: Record<string, string>;
|
|
183
|
+
/** PUBLIC_* (excluding PUBLIC_STATIC_*) — client+server, runtime */
|
|
184
|
+
publicDynamic: Record<string, string>;
|
|
185
|
+
/** STATIC_* — server only, build-time inlined */
|
|
186
|
+
privateStatic: Record<string, string>;
|
|
187
|
+
/** everything else — server only, runtime */
|
|
188
|
+
privateDynamic: Record<string, string>;
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
/** Classify env vars by prefix into the four visibility/timing buckets. */
|
|
185
192
|
export function classifyEnvVars(env: Record<string, string>): ClassifiedEnv {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
const publicStatic: Record<string, string> = {};
|
|
194
|
+
const publicDynamic: Record<string, string> = {};
|
|
195
|
+
const privateStatic: Record<string, string> = {};
|
|
196
|
+
const privateDynamic: Record<string, string> = {};
|
|
197
|
+
|
|
198
|
+
for (const [key, value] of Object.entries(env)) {
|
|
199
|
+
if (key.startsWith("PUBLIC_STATIC_")) {
|
|
200
|
+
publicStatic[key] = value;
|
|
201
|
+
} else if (key.startsWith("PUBLIC_")) {
|
|
202
|
+
publicDynamic[key] = value;
|
|
203
|
+
} else if (key.startsWith("STATIC_")) {
|
|
204
|
+
privateStatic[key] = value;
|
|
205
|
+
} else {
|
|
206
|
+
privateDynamic[key] = value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { publicStatic, publicDynamic, privateStatic, privateDynamic };
|
|
204
211
|
}
|
package/src/core/envCodegen.ts
CHANGED
|
@@ -9,86 +9,86 @@ import type { ClassifiedEnv } from "./env.ts";
|
|
|
9
9
|
// types/env.d.ts — declare module '$env' for IDE autocomplete
|
|
10
10
|
|
|
11
11
|
export function generateEnvModules(classified: ClassifiedEnv): void {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
const bosiaDir = join(process.cwd(), ".bosia");
|
|
13
|
+
const typesDir = join(bosiaDir, "types");
|
|
14
|
+
mkdirSync(bosiaDir, { recursive: true });
|
|
15
|
+
mkdirSync(typesDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
writeServerEnv(classified, bosiaDir);
|
|
18
|
+
writeClientEnv(classified, bosiaDir);
|
|
19
|
+
writeEnvTypes(classified, typesDir);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function writeServerEnv(classified: ClassifiedEnv, bosiaDir: string): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
23
|
+
const lines: string[] = [
|
|
24
|
+
"// Auto-generated by Bosia. Do not edit.",
|
|
25
|
+
"// $env → server — all vars",
|
|
26
|
+
"",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// PUBLIC_STATIC_* — inlined literals
|
|
30
|
+
for (const [key, value] of Object.entries(classified.publicStatic)) {
|
|
31
|
+
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// PUBLIC_* dynamic — read from process.env at runtime
|
|
35
|
+
for (const key of Object.keys(classified.publicDynamic)) {
|
|
36
|
+
lines.push(`export const ${key} = process.env.${key} ?? "";`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// STATIC_* — inlined literals
|
|
40
|
+
for (const [key, value] of Object.entries(classified.privateStatic)) {
|
|
41
|
+
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// private dynamic — read from process.env at runtime
|
|
45
|
+
for (const key of Object.keys(classified.privateDynamic)) {
|
|
46
|
+
lines.push(`export const ${key} = process.env.${key} ?? "";`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
writeFileSync(join(bosiaDir, "env.server.ts"), lines.join("\n") + "\n");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function writeClientEnv(classified: ClassifiedEnv, bosiaDir: string): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
53
|
+
const lines: string[] = [
|
|
54
|
+
"// Auto-generated by Bosia. Do not edit.",
|
|
55
|
+
"// $env → client — PUBLIC_* vars only",
|
|
56
|
+
"",
|
|
57
|
+
"const __env: Record<string, string> =",
|
|
58
|
+
" typeof window !== 'undefined' && (window as any).__BOSIA_ENV__ || {};",
|
|
59
|
+
"",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// PUBLIC_STATIC_* — inlined literals
|
|
63
|
+
for (const [key, value] of Object.entries(classified.publicStatic)) {
|
|
64
|
+
lines.push(`export const ${key} = ${JSON.stringify(value)};`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PUBLIC_* dynamic — read from window.__BOSIA_ENV__ at runtime
|
|
68
|
+
for (const key of Object.keys(classified.publicDynamic)) {
|
|
69
|
+
lines.push(`export const ${key} = __env.${key} ?? "";`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
writeFileSync(join(bosiaDir, "env.client.ts"), lines.join("\n") + "\n");
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function writeEnvTypes(classified: ClassifiedEnv, typesDir: string): void {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
76
|
+
const allKeys = [
|
|
77
|
+
...Object.keys(classified.publicStatic),
|
|
78
|
+
...Object.keys(classified.publicDynamic),
|
|
79
|
+
...Object.keys(classified.privateStatic),
|
|
80
|
+
...Object.keys(classified.privateDynamic),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const declarations = allKeys.map((key) => ` export const ${key}: string;`);
|
|
84
|
+
|
|
85
|
+
const content = [
|
|
86
|
+
"// Auto-generated by Bosia. Do not edit.",
|
|
87
|
+
"declare module '$env' {",
|
|
88
|
+
...declarations,
|
|
89
|
+
"}",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
|
|
93
|
+
writeFileSync(join(typesDir, "env.d.ts"), content);
|
|
94
94
|
}
|
package/src/core/errors.ts
CHANGED
|
@@ -2,85 +2,84 @@
|
|
|
2
2
|
// Throw these from load() functions; the server catches and handles them.
|
|
3
3
|
|
|
4
4
|
export class HttpError extends Error {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
constructor(
|
|
6
|
+
public status: number,
|
|
7
|
+
message: string,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "HttpError";
|
|
11
|
+
}
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export interface RedirectOptions {
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
/** Set to `true` to allow redirects to external origins (e.g. OAuth providers). */
|
|
16
|
+
allowExternal?: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export class Redirect {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
constructor(
|
|
21
|
+
public status: number,
|
|
22
|
+
public location: string,
|
|
23
|
+
options?: RedirectOptions,
|
|
24
|
+
) {
|
|
25
|
+
validateRedirectLocation(location, options);
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
const DANGEROUS_SCHEMES = /^(javascript|data|vbscript):/i;
|
|
27
30
|
|
|
28
|
-
function validateRedirectLocation(
|
|
29
|
-
|
|
30
|
-
options?: RedirectOptions,
|
|
31
|
-
): void {
|
|
32
|
-
if (options?.allowExternal) return;
|
|
31
|
+
function validateRedirectLocation(location: string, options?: RedirectOptions): void {
|
|
32
|
+
if (options?.allowExternal) return;
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const trimmed = location.trim();
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
// Reject dangerous schemes
|
|
37
|
+
if (DANGEROUS_SCHEMES.test(trimmed)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`redirect(): dangerous scheme in URL "${location}". ` +
|
|
40
|
+
`Only relative paths and same-origin URLs are allowed.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// Reject protocol-relative URLs (//evil.com)
|
|
45
|
+
if (trimmed.startsWith("//")) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`redirect(): protocol-relative URLs like "${location}" are not allowed. ` +
|
|
48
|
+
`Use a relative path or pass { allowExternal: true } for external redirects.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Allow relative paths (no scheme)
|
|
53
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(trimmed)) return;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
// It's an absolute URL — reject external origins
|
|
56
|
+
throw new Error(
|
|
57
|
+
`redirect(): external URL "${location}" is not allowed. ` +
|
|
58
|
+
`Use a relative path or pass { allowExternal: true } for external redirects.`,
|
|
59
|
+
);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Throw an HTTP error from a load() function. */
|
|
63
63
|
export function error(status: number, message: string): never {
|
|
64
|
-
|
|
64
|
+
throw new HttpError(status, message);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** Redirect the user from a load() function. */
|
|
68
|
-
export function redirect(
|
|
69
|
-
|
|
70
|
-
location: string,
|
|
71
|
-
options?: RedirectOptions,
|
|
72
|
-
): never {
|
|
73
|
-
throw new Redirect(status, location, options);
|
|
68
|
+
export function redirect(status: number, location: string, options?: RedirectOptions): never {
|
|
69
|
+
throw new Redirect(status, location, options);
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
// ─── Form Action Helpers ─────────────────────────────────
|
|
77
73
|
// Return from form actions — not thrown, just returned.
|
|
78
74
|
|
|
79
75
|
export class ActionFailure<T extends Record<string, any> = Record<string, any>> {
|
|
80
|
-
|
|
76
|
+
constructor(
|
|
77
|
+
public status: number,
|
|
78
|
+
public data: T,
|
|
79
|
+
) {}
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
/** Return a failure from a form action with a status code and data. */
|
|
84
83
|
export function fail<T extends Record<string, any>>(status: number, data: T): ActionFailure<T> {
|
|
85
|
-
|
|
84
|
+
return new ActionFailure(status, data);
|
|
86
85
|
}
|