bosia 0.2.2 → 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.
Files changed (87) hide show
  1. package/README.md +39 -39
  2. package/package.json +56 -53
  3. package/src/ambient.d.ts +31 -0
  4. package/src/cli/add.ts +120 -114
  5. package/src/cli/build.ts +10 -10
  6. package/src/cli/create.ts +142 -137
  7. package/src/cli/dev.ts +8 -8
  8. package/src/cli/feat.ts +291 -132
  9. package/src/cli/index.ts +51 -42
  10. package/src/cli/registry.ts +136 -115
  11. package/src/cli/start.ts +17 -17
  12. package/src/cli/test.ts +25 -0
  13. package/src/core/build.ts +72 -56
  14. package/src/core/client/App.svelte +177 -153
  15. package/src/core/client/appState.svelte.ts +57 -0
  16. package/src/core/client/enhance.ts +112 -0
  17. package/src/core/client/hydrate.ts +97 -65
  18. package/src/core/client/prefetch.ts +101 -94
  19. package/src/core/client/router.svelte.ts +64 -51
  20. package/src/core/cookies.ts +70 -66
  21. package/src/core/cors.ts +44 -35
  22. package/src/core/csrf.ts +38 -38
  23. package/src/core/dedup.ts +17 -17
  24. package/src/core/dev.ts +165 -168
  25. package/src/core/env.ts +155 -128
  26. package/src/core/envCodegen.ts +73 -73
  27. package/src/core/errors.ts +48 -49
  28. package/src/core/hooks.ts +50 -50
  29. package/src/core/html.ts +192 -139
  30. package/src/core/matcher.ts +130 -121
  31. package/src/core/paths.ts +8 -10
  32. package/src/core/plugin.ts +113 -107
  33. package/src/core/prerender.ts +191 -118
  34. package/src/core/renderer.ts +359 -265
  35. package/src/core/routeFile.ts +140 -127
  36. package/src/core/routeTypes.ts +144 -83
  37. package/src/core/scanner.ts +125 -95
  38. package/src/core/server.ts +543 -370
  39. package/src/core/types.ts +25 -20
  40. package/src/lib/client.ts +12 -0
  41. package/src/lib/index.ts +8 -8
  42. package/src/lib/utils.ts +44 -30
  43. package/templates/default/.prettierignore +5 -0
  44. package/templates/default/.prettierrc.json +9 -0
  45. package/templates/default/README.md +5 -5
  46. package/templates/default/package.json +22 -18
  47. package/templates/default/src/app.css +80 -80
  48. package/templates/default/src/app.d.ts +3 -3
  49. package/templates/default/src/routes/+error.svelte +7 -10
  50. package/templates/default/src/routes/+layout.svelte +2 -2
  51. package/templates/default/src/routes/+page.svelte +31 -29
  52. package/templates/default/src/routes/about/+page.svelte +3 -3
  53. package/templates/default/tsconfig.json +20 -20
  54. package/templates/demo/.prettierignore +5 -0
  55. package/templates/demo/.prettierrc.json +9 -0
  56. package/templates/demo/README.md +9 -9
  57. package/templates/demo/package.json +22 -17
  58. package/templates/demo/src/app.css +80 -80
  59. package/templates/demo/src/app.d.ts +3 -3
  60. package/templates/demo/src/hooks.server.ts +9 -9
  61. package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
  62. package/templates/demo/src/routes/(public)/+page.svelte +96 -67
  63. package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
  64. package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
  65. package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
  66. package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
  67. package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
  68. package/templates/demo/src/routes/+error.svelte +10 -7
  69. package/templates/demo/src/routes/+layout.server.ts +4 -4
  70. package/templates/demo/src/routes/+layout.svelte +2 -2
  71. package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
  72. package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
  73. package/templates/demo/src/routes/api/hello/+server.ts +25 -25
  74. package/templates/demo/tsconfig.json +20 -20
  75. package/templates/todo/.prettierignore +5 -0
  76. package/templates/todo/.prettierrc.json +9 -0
  77. package/templates/todo/README.md +9 -9
  78. package/templates/todo/package.json +22 -17
  79. package/templates/todo/src/app.css +80 -80
  80. package/templates/todo/src/app.d.ts +7 -7
  81. package/templates/todo/src/hooks.server.ts +9 -9
  82. package/templates/todo/src/routes/+error.svelte +10 -7
  83. package/templates/todo/src/routes/+layout.server.ts +4 -4
  84. package/templates/todo/src/routes/+layout.svelte +2 -2
  85. package/templates/todo/src/routes/+page.svelte +44 -44
  86. package/templates/todo/template.json +1 -1
  87. 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
- "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",
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,51 +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
- return raw.replace(/\\(.)/g, (_, ch) => {
31
- switch (ch) {
32
- case "n": return "\n";
33
- case "r": return "\r";
34
- case "t": return "\t";
35
- case "\\": return "\\";
36
- case '"': return '"';
37
- default: return `\\${ch}`; // preserve unknown escapes
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
- const result: Record<string, string> = {};
45
- const lines = content.split("\n");
46
- for (let i = 0; i < lines.length; i++) {
47
- const trimmed = lines[i].trim();
48
- if (!trimmed || trimmed.startsWith("#")) continue;
49
- const eqIdx = trimmed.indexOf("=");
50
- if (eqIdx === -1) continue;
51
- const key = trimmed.slice(0, eqIdx).trim();
52
- if (!key) continue;
53
-
54
- // Validate key is a valid identifier (required for codegen)
55
- if (!VALID_ENV_NAME.test(key)) {
56
- const loc = filename ? ` in ${filename}` : "";
57
- throw new Error(
58
- `Invalid env variable name "${key}"${loc} (line ${i + 1}). ` +
59
- `Names must start with a letter or underscore and contain only [A-Za-z0-9_].`
60
- );
61
- }
62
-
63
- let value = trimmed.slice(eqIdx + 1).trim();
64
- // Double-quoted: process escape sequences
65
- if (value.startsWith('"') && value.endsWith('"')) {
66
- value = processEscapes(value.slice(1, -1));
67
- }
68
- // Single-quoted: literal (no escape processing)
69
- else if (value.startsWith("'") && value.endsWith("'")) {
70
- value = value.slice(1, -1);
71
- }
72
- result[key] = value;
73
- }
74
- return result;
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;
75
107
  }
76
108
 
77
109
  // ─── Env Loader ──────────────────────────────────────────
@@ -91,51 +123,46 @@ function parseEnvFile(content: string, filename?: string): Record<string, string
91
123
  * @returns merged env record (only vars from .env files, excluding framework vars)
92
124
  */
93
125
  export function loadEnv(mode: string, dir?: string): Record<string, string> {
94
- const root = dir ?? process.cwd();
95
- const files = [
96
- ".env",
97
- ".env.local",
98
- `.env.${mode}`,
99
- `.env.${mode}.local`,
100
- ];
101
-
102
- let merged: Record<string, string> = {};
103
- const loaded: string[] = [];
104
-
105
- for (const filename of files) {
106
- const filepath = join(root, filename);
107
- if (!existsSync(filepath)) continue;
108
- const content = readFileSync(filepath, "utf-8");
109
- const parsed = parseEnvFile(content, filename);
110
- merged = { ...merged, ...parsed };
111
- loaded.push(filename);
112
- }
113
-
114
- if (loaded.length > 0) {
115
- console.log(`✓ Loaded ${loaded.join(", ")}`);
116
- }
117
-
118
- // Track declared keys so html.ts only exposes .env-declared PUBLIC_* vars
119
- for (const key of Object.keys(merged)) {
120
- _declaredKeys.add(key);
121
- }
122
-
123
- // Apply to process.env — system env wins (don't overwrite existing)
124
- for (const [key, value] of Object.entries(merged)) {
125
- if (!(key in process.env)) {
126
- process.env[key] = value;
127
- }
128
- }
129
-
130
- // Return only non-framework vars
131
- const result: Record<string, string> = {};
132
- for (const [key, value] of Object.entries(merged)) {
133
- if (!FRAMEWORK_VARS.has(key)) {
134
- result[key] = process.env[key] ?? value;
135
- }
136
- }
137
-
138
- 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;
139
166
  }
140
167
 
141
168
  // ─── Declared Key Tracking ───────────────────────────
@@ -145,40 +172,40 @@ const _declaredKeys = new Set<string>();
145
172
 
146
173
  /** Returns the set of env var keys that were declared in .env files. */
147
174
  export function getDeclaredEnvKeys(): ReadonlySet<string> {
148
- return _declaredKeys;
175
+ return _declaredKeys;
149
176
  }
150
177
 
151
178
  // ─── Classifier ──────────────────────────────────────────
152
179
 
153
180
  export interface ClassifiedEnv {
154
- /** PUBLIC_STATIC_* — client+server, build-time inlined */
155
- publicStatic: Record<string, string>;
156
- /** PUBLIC_* (excluding PUBLIC_STATIC_*) — client+server, runtime */
157
- publicDynamic: Record<string, string>;
158
- /** STATIC_* — server only, build-time inlined */
159
- privateStatic: Record<string, string>;
160
- /** everything else — server only, runtime */
161
- privateDynamic: Record<string, string>;
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>;
162
189
  }
163
190
 
164
191
  /** Classify env vars by prefix into the four visibility/timing buckets. */
165
192
  export function classifyEnvVars(env: Record<string, string>): ClassifiedEnv {
166
- const publicStatic: Record<string, string> = {};
167
- const publicDynamic: Record<string, string> = {};
168
- const privateStatic: Record<string, string> = {};
169
- const privateDynamic: Record<string, string> = {};
170
-
171
- for (const [key, value] of Object.entries(env)) {
172
- if (key.startsWith("PUBLIC_STATIC_")) {
173
- publicStatic[key] = value;
174
- } else if (key.startsWith("PUBLIC_")) {
175
- publicDynamic[key] = value;
176
- } else if (key.startsWith("STATIC_")) {
177
- privateStatic[key] = value;
178
- } else {
179
- privateDynamic[key] = value;
180
- }
181
- }
182
-
183
- return { publicStatic, publicDynamic, privateStatic, privateDynamic };
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 };
184
211
  }
@@ -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
- 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);
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
- 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");
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
- 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");
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
- 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);
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
  }
@@ -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
- constructor(public status: number, message: string) {
6
- super(message);
7
- this.name = "HttpError";
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
- /** Set to `true` to allow redirects to external origins (e.g. OAuth providers). */
13
- allowExternal?: boolean;
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
- constructor(
18
- public status: number,
19
- public location: string,
20
- options?: RedirectOptions,
21
- ) {
22
- validateRedirectLocation(location, options);
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
- location: string,
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
- const trimmed = location.trim();
34
+ const trimmed = location.trim();
35
35
 
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
- }
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
- // 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
- }
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
- // Allow relative paths (no scheme)
53
- if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(trimmed)) return;
52
+ // Allow relative paths (no scheme)
53
+ if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(trimmed)) return;
54
54
 
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
- );
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
- throw new HttpError(status, message);
64
+ throw new HttpError(status, message);
65
65
  }
66
66
 
67
67
  /** Redirect the user from a load() function. */
68
- export function redirect(
69
- status: number,
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
- constructor(public status: number, public data: T) {}
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
- return new ActionFailure(status, data);
84
+ return new ActionFailure(status, data);
86
85
  }