bosia 0.6.21 → 0.6.23
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/cli/add.ts +3 -4
- package/src/cli/block.ts +16 -10
- package/src/cli/create.ts +6 -11
- package/src/cli/feat.ts +19 -22
- package/src/cli/index.ts +1 -2
- package/src/cli/manifest.ts +1 -1
- package/src/cli/registry.ts +3 -1
- package/src/core/build.ts +1 -3
- package/src/core/client/App.svelte +3 -8
- package/src/core/client/router.svelte.ts +3 -8
- package/src/core/config.ts +1 -4
- package/src/core/cookies.ts +1 -2
- package/src/core/dev-500.ts +1 -1
- package/src/core/html.ts +1 -2
- package/src/core/plugin.ts +1 -3
- package/src/core/plugins/inspector/bun-plugin.ts +1 -4
- package/src/core/plugins/inspector/index.ts +45 -59
- package/src/core/renderer.ts +3 -10
- package/src/core/routeTypes.ts +3 -9
- package/src/core/scanner.ts +1 -3
- package/src/core/server.ts +30 -35
- package/src/core/staticManifest.ts +1 -3
- package/src/core/svelteAudit.ts +2 -5
- package/src/core/svelteCompiler.ts +2 -8
- package/templates/default/.prettierignore +1 -0
- package/templates/default/src/app.css +2 -0
- package/templates/demo/.prettierignore +1 -0
- package/templates/demo/src/app.css +2 -0
- package/templates/shop/.env.example +12 -0
- package/templates/shop/.prettierignore +7 -0
- package/templates/shop/.prettierrc.json +9 -0
- package/templates/shop/README.md +62 -0
- package/templates/shop/_gitignore +12 -0
- package/templates/shop/bosia.config.ts +10 -0
- package/templates/shop/instructions.txt +8 -0
- package/templates/shop/package.json +26 -0
- package/templates/shop/public/favicon.svg +14 -0
- package/templates/shop/public/logo-dark.svg +14 -0
- package/templates/shop/public/logo-light.svg +14 -0
- package/templates/shop/src/app.css +134 -0
- package/templates/shop/src/app.d.ts +14 -0
- package/templates/shop/src/app.html +11 -0
- package/templates/shop/src/hooks.server.ts +21 -0
- package/templates/shop/src/lib/utils.ts +1 -0
- package/templates/shop/src/routes/(private)/+layout.server.ts +10 -0
- package/templates/shop/src/routes/(private)/+layout.svelte +44 -0
- package/templates/shop/src/routes/(private)/dashboard/+page.svelte +11 -0
- package/templates/shop/src/routes/(public)/+layout.svelte +13 -0
- package/templates/shop/src/routes/(public)/+page.svelte +38 -0
- package/templates/shop/src/routes/+error.svelte +19 -0
- package/templates/shop/src/routes/+layout.server.ts +9 -0
- package/templates/shop/src/routes/+layout.svelte +6 -0
- package/templates/shop/template.json +10 -0
- package/templates/shop/tsconfig.json +22 -0
- package/templates/todo/.prettierignore +1 -0
- package/templates/todo/src/app.css +2 -0
- package/templates/todo/template.json +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosia",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing inspired by SvelteKit. No Node.js, no Vite, no adapters.",
|
|
6
6
|
"keywords": [
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@tailwindcss/cli": "^4.2.1",
|
|
57
57
|
"elysia": "^1.4.26",
|
|
58
58
|
"magic-string": "^0.30.0",
|
|
59
|
-
"svelte": "^5.
|
|
59
|
+
"svelte": "^5.56.3",
|
|
60
60
|
"tailwind-merge": "^3.5.0",
|
|
61
61
|
"tailwindcss": "^4.2.1"
|
|
62
62
|
}
|
package/src/cli/add.ts
CHANGED
|
@@ -159,8 +159,7 @@ export async function addComponent(name: string, root = false, options?: Install
|
|
|
159
159
|
if (Object.keys(meta.npmDeps).length > 0) {
|
|
160
160
|
if (options?.skipInstall) {
|
|
161
161
|
const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
|
|
162
|
-
if (addedDeps.length > 0)
|
|
163
|
-
console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
162
|
+
if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
164
163
|
} else {
|
|
165
164
|
await bunAdd(cwd, meta.npmDeps);
|
|
166
165
|
}
|
|
@@ -239,8 +238,8 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
239
238
|
}
|
|
240
239
|
`;
|
|
241
240
|
|
|
242
|
-
export function ensureUtils() {
|
|
243
|
-
const utilsPath = join(
|
|
241
|
+
export function ensureUtils(cwd: string = process.cwd()) {
|
|
242
|
+
const utilsPath = join(cwd, "src", "lib", "utils.ts");
|
|
244
243
|
if (!existsSync(utilsPath)) {
|
|
245
244
|
mkdirSync(dirname(utilsPath), { recursive: true });
|
|
246
245
|
writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
|
package/src/cli/block.ts
CHANGED
|
@@ -44,24 +44,33 @@ export async function runAddBlock(
|
|
|
44
44
|
|
|
45
45
|
const local = flags.includes("--local");
|
|
46
46
|
const flagYes = flags.includes("-y") || flags.includes("--yes");
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Honor an inherited registry root from options (e.g. when called from feat.ts in --local mode).
|
|
48
|
+
const inheritedRoot = options?.registryRoot ?? null;
|
|
49
|
+
const registryRoot = inheritedRoot ?? (local ? resolveLocalRegistryOrExit() : null);
|
|
50
|
+
if (local && !inheritedRoot) console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
49
51
|
|
|
50
52
|
const resolvedOptions: InstallOptions = {
|
|
51
53
|
...(options ?? {}),
|
|
54
|
+
registryRoot,
|
|
52
55
|
skipPrompts: options?.skipPrompts ?? flagYes,
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
await initAddRegistry(registryRoot);
|
|
56
|
-
ensureUtils();
|
|
59
|
+
ensureUtils(resolvedOptions.cwd);
|
|
57
60
|
|
|
58
61
|
console.log(`⬡ Installing block: ${name}\n`);
|
|
59
62
|
|
|
60
63
|
const meta = await readRegistryJSON<BlockMeta>(registryRoot, "blocks", name, "meta.json");
|
|
61
64
|
|
|
62
|
-
// 1. Install primitive dependencies first
|
|
65
|
+
// 1. Install primitive dependencies first.
|
|
66
|
+
// Component deps (e.g. "ui/button") go through addComponent.
|
|
67
|
+
// Block deps (e.g. "blocks/files/upload-area") recurse into runAddBlock.
|
|
63
68
|
for (const dep of meta.dependencies ?? []) {
|
|
64
|
-
|
|
69
|
+
if (dep.startsWith("blocks/")) {
|
|
70
|
+
await runAddBlock(dep.slice("blocks/".length), [], resolvedOptions);
|
|
71
|
+
} else {
|
|
72
|
+
await addComponent(dep, false, resolvedOptions);
|
|
73
|
+
}
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
// 2. Copy block files to src/lib/blocks/<path>/
|
|
@@ -103,8 +112,7 @@ export async function runAddBlock(
|
|
|
103
112
|
if (meta.npmDeps && Object.keys(meta.npmDeps).length > 0) {
|
|
104
113
|
if (resolvedOptions.skipInstall) {
|
|
105
114
|
const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
|
|
106
|
-
if (addedDeps.length > 0)
|
|
107
|
-
console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
115
|
+
if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
108
116
|
} else {
|
|
109
117
|
await bunAdd(cwd, meta.npmDeps);
|
|
110
118
|
}
|
|
@@ -119,9 +127,7 @@ export async function runAddBlock(
|
|
|
119
127
|
...(meta.dependencies && meta.dependencies.length > 0
|
|
120
128
|
? { dependencies: meta.dependencies }
|
|
121
129
|
: {}),
|
|
122
|
-
...(meta.fonts && Object.keys(meta.fonts).length > 0
|
|
123
|
-
? { fonts: Object.keys(meta.fonts) }
|
|
124
|
-
: {}),
|
|
130
|
+
...(meta.fonts && Object.keys(meta.fonts).length > 0 ? { fonts: Object.keys(meta.fonts) } : {}),
|
|
125
131
|
});
|
|
126
132
|
|
|
127
133
|
console.log(`\n✅ ${name} installed at src/lib/blocks/${name}/`);
|
package/src/cli/create.ts
CHANGED
|
@@ -15,13 +15,12 @@ const TEMPLATE_DESCRIPTIONS: Record<string, string> = {
|
|
|
15
15
|
default: "Minimal starter with routing and Tailwind",
|
|
16
16
|
demo: "Full-featured demo with hooks, API routes, form actions, and more",
|
|
17
17
|
todo: "Todo app with PostgreSQL + Drizzle ORM",
|
|
18
|
+
shop: "Online store starter with auth, RBAC, S3 uploads, products/orders/cart",
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
21
22
|
if (!name) {
|
|
22
|
-
console.error(
|
|
23
|
-
"❌ Please provide a project name.\n Usage: bun x bosia@latest create my-app",
|
|
24
|
-
);
|
|
23
|
+
console.error("❌ Please provide a project name.\n Usage: bun x bosia@latest create my-app");
|
|
25
24
|
process.exit(1);
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -68,10 +67,7 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
68
67
|
copyDir(templateDir, targetDir, name, isLocal);
|
|
69
68
|
|
|
70
69
|
if (existsSync(join(targetDir, ".env.example"))) {
|
|
71
|
-
writeFileSync(
|
|
72
|
-
join(targetDir, ".env"),
|
|
73
|
-
readFileSync(join(targetDir, ".env.example"), "utf-8"),
|
|
74
|
-
);
|
|
70
|
+
writeFileSync(join(targetDir, ".env"), readFileSync(join(targetDir, ".env.example"), "utf-8"));
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
// Install template features from registry
|
|
@@ -89,11 +85,13 @@ export async function runCreate(name: string | undefined, args: string[] = []) {
|
|
|
89
85
|
await initAddRegistry(localRegistry);
|
|
90
86
|
initFeatRegistry(localRegistry);
|
|
91
87
|
|
|
88
|
+
const featureOptions: Record<string, string> = config.featureOptions ?? {};
|
|
92
89
|
for (const feat of config.features) {
|
|
93
90
|
await installFeature(feat, true, {
|
|
94
91
|
skipInstall: true,
|
|
95
92
|
skipPrompts: true,
|
|
96
93
|
cwd: targetDir,
|
|
94
|
+
featureOptions,
|
|
97
95
|
});
|
|
98
96
|
}
|
|
99
97
|
}
|
|
@@ -177,10 +175,7 @@ function copyDir(src: string, dest: string, projectName: string, isLocal: boolea
|
|
|
177
175
|
if (entry.isDirectory()) {
|
|
178
176
|
copyDir(srcPath, destPath, projectName, isLocal);
|
|
179
177
|
} else {
|
|
180
|
-
let content = readFileSync(srcPath, "utf-8").replaceAll(
|
|
181
|
-
"{{PROJECT_NAME}}",
|
|
182
|
-
projectName,
|
|
183
|
-
);
|
|
178
|
+
let content = readFileSync(srcPath, "utf-8").replaceAll("{{PROJECT_NAME}}", projectName);
|
|
184
179
|
|
|
185
180
|
if (entry.name === "package.json" && isLocal) {
|
|
186
181
|
const bosiaPath = resolve(import.meta.dir, "../../");
|
package/src/cli/feat.ts
CHANGED
|
@@ -109,8 +109,11 @@ async function resolveFeatureOptions(
|
|
|
109
109
|
options: FeatureOption[],
|
|
110
110
|
args: string[],
|
|
111
111
|
skipPrompts: boolean,
|
|
112
|
+
seed: Record<string, string> = {},
|
|
112
113
|
): Promise<Record<string, string>> {
|
|
113
|
-
|
|
114
|
+
// Seed values come from inherited featureOptions (e.g. template-level defaults).
|
|
115
|
+
// They beat per-feature `default` but lose to explicit CLI args.
|
|
116
|
+
const values: Record<string, string> = { ...seed };
|
|
114
117
|
const byFlag = new Map<string, FeatureOption>();
|
|
115
118
|
for (const opt of options) {
|
|
116
119
|
if (opt.flag) byFlag.set(opt.flag, opt);
|
|
@@ -153,9 +156,7 @@ async function resolveFeatureOptions(
|
|
|
153
156
|
continue;
|
|
154
157
|
}
|
|
155
158
|
if (opt.required) {
|
|
156
|
-
console.error(
|
|
157
|
-
`❌ Feature "${featName}" requires "${opt.flag ?? opt.long ?? opt.name}".`,
|
|
158
|
-
);
|
|
159
|
+
console.error(`❌ Feature "${featName}" requires "${opt.flag ?? opt.long ?? opt.name}".`);
|
|
159
160
|
process.exit(1);
|
|
160
161
|
}
|
|
161
162
|
continue;
|
|
@@ -217,19 +218,22 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
|
|
|
217
218
|
const inheritedOptions = options?.featureOptions ?? {};
|
|
218
219
|
let myOptions: Record<string, string> = {};
|
|
219
220
|
if (meta.options && meta.options.length > 0) {
|
|
221
|
+
// Extract this feature's seed values from the namespaced inherited map.
|
|
222
|
+
const seed: Record<string, string> = {};
|
|
223
|
+
for (const [k, v] of Object.entries(inheritedOptions)) {
|
|
224
|
+
const [feat, optName] = k.split(".");
|
|
225
|
+
if (feat === name) seed[optName] = v;
|
|
226
|
+
}
|
|
220
227
|
myOptions = isRoot
|
|
221
228
|
? await resolveFeatureOptions(
|
|
222
229
|
name,
|
|
223
230
|
meta.options,
|
|
224
231
|
options?.featureArgs ?? [],
|
|
225
232
|
options?.skipPrompts ?? false,
|
|
233
|
+
seed,
|
|
226
234
|
)
|
|
227
235
|
: // Dependency features inherit any caller-provided values; prompt only for unresolved required opts.
|
|
228
|
-
await resolveFeatureOptions(name, meta.options, [], options?.skipPrompts ?? false);
|
|
229
|
-
for (const [k, v] of Object.entries(inheritedOptions)) {
|
|
230
|
-
const [feat, optName] = k.split(".");
|
|
231
|
-
if (feat === name && !(optName in myOptions)) myOptions[optName] = v;
|
|
232
|
-
}
|
|
236
|
+
await resolveFeatureOptions(name, meta.options, [], options?.skipPrompts ?? false, seed);
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
// Merge into the namespaced map for downstream dependency features.
|
|
@@ -262,7 +266,7 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
|
|
|
262
266
|
if (meta.blocks && meta.blocks.length > 0) {
|
|
263
267
|
console.log("🧱 Installing required blocks...");
|
|
264
268
|
for (const blockName of meta.blocks) {
|
|
265
|
-
await runAddBlock(blockName, [], options);
|
|
269
|
+
await runAddBlock(blockName, [], { ...options, registryRoot });
|
|
266
270
|
}
|
|
267
271
|
console.log("");
|
|
268
272
|
}
|
|
@@ -308,16 +312,13 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
|
|
|
308
312
|
devDeps: meta.npmDevDeps,
|
|
309
313
|
scripts: meta.scripts,
|
|
310
314
|
});
|
|
311
|
-
if (addedDeps.length > 0)
|
|
312
|
-
|
|
313
|
-
if (addedScripts.length > 0)
|
|
314
|
-
console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
315
|
+
if (addedDeps.length > 0) console.log(`\n📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
316
|
+
if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
315
317
|
} else {
|
|
316
318
|
await bunAdd(cwd, meta.npmDeps, meta.npmDevDeps);
|
|
317
319
|
if (hasScripts) {
|
|
318
320
|
const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
|
|
319
|
-
if (addedScripts.length > 0)
|
|
320
|
-
console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
321
|
+
if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
} else if (hasScripts) {
|
|
@@ -359,9 +360,7 @@ export async function installFeature(name: string, isRoot: boolean, options?: In
|
|
|
359
360
|
(meta.blocks && meta.blocks.length > 0)
|
|
360
361
|
? {
|
|
361
362
|
deps: {
|
|
362
|
-
...(meta.features && meta.features.length > 0
|
|
363
|
-
? { features: meta.features }
|
|
364
|
-
: {}),
|
|
363
|
+
...(meta.features && meta.features.length > 0 ? { features: meta.features } : {}),
|
|
365
364
|
...(meta.components.length > 0 ? { components: meta.components } : {}),
|
|
366
365
|
...(meta.blocks && meta.blocks.length > 0 ? { blocks: meta.blocks } : {}),
|
|
367
366
|
},
|
|
@@ -439,9 +438,7 @@ async function applyStrategy(args: StrategyArgs): Promise<void> {
|
|
|
439
438
|
|
|
440
439
|
const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
441
440
|
writeFileSync(dest, existing + nl + newLines.join("\n") + "\n", "utf-8");
|
|
442
|
-
console.log(
|
|
443
|
-
` ➕ ${target} (+${newLines.length} line${newLines.length === 1 ? "" : "s"})`,
|
|
444
|
-
);
|
|
441
|
+
console.log(` ➕ ${target} (+${newLines.length} line${newLines.length === 1 ? "" : "s"})`);
|
|
445
442
|
return;
|
|
446
443
|
}
|
|
447
444
|
|
package/src/cli/index.ts
CHANGED
|
@@ -96,8 +96,7 @@ async function main() {
|
|
|
96
96
|
// First non-flag token is the feature name; everything else flows through to the
|
|
97
97
|
// feature's own option parser. Global flags (-y, --local) are also accepted here
|
|
98
98
|
// and get split out inside runFeat.
|
|
99
|
-
const rest =
|
|
100
|
-
nameIdx === -1 ? args : [...args.slice(0, nameIdx), ...args.slice(nameIdx + 1)];
|
|
99
|
+
const rest = nameIdx === -1 ? args : [...args.slice(0, nameIdx), ...args.slice(nameIdx + 1)];
|
|
101
100
|
await runFeat(featName, rest);
|
|
102
101
|
break;
|
|
103
102
|
}
|
package/src/cli/manifest.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function readManifest(cwd: string = process.cwd()): Manifest {
|
|
|
67
67
|
|
|
68
68
|
export function writeManifest(manifest: Manifest, cwd: string = process.cwd()): void {
|
|
69
69
|
const path = join(cwd, MANIFEST_FILE);
|
|
70
|
-
writeFileSync(path, JSON.stringify(manifest, null,
|
|
70
|
+
writeFileSync(path, JSON.stringify(manifest, null, "\t") + "\n", "utf-8");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export function recordFeature(
|
package/src/cli/registry.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface InstallOptions {
|
|
|
10
10
|
skipInstall?: boolean; // write deps to package.json instead of `bun add`
|
|
11
11
|
skipPrompts?: boolean; // auto-overwrite, no interactive prompts
|
|
12
12
|
cwd?: string; // override process.cwd() for file operations
|
|
13
|
+
/** When set, use this absolute path as the local registry root instead of fetching from GitHub. */
|
|
14
|
+
registryRoot?: string | null;
|
|
13
15
|
/** Pre-resolved feature-specific option values, keyed by `featureName.optionName`. */
|
|
14
16
|
featureOptions?: Record<string, string>;
|
|
15
17
|
/** Remaining argv tokens to be parsed as the root feature's own options. */
|
|
@@ -174,7 +176,7 @@ export function mergePkgJson(
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
if (changed) {
|
|
177
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null,
|
|
179
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n", "utf-8");
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
return { addedDeps, addedScripts };
|
package/src/core/build.ts
CHANGED
|
@@ -88,9 +88,7 @@ let appHtml: any;
|
|
|
88
88
|
try {
|
|
89
89
|
appHtml = loadAppHtmlTemplate(process.cwd());
|
|
90
90
|
console.log(
|
|
91
|
-
"📄 Loaded src/app.html (favicon override: " +
|
|
92
|
-
(appHtml.hasCustomFavicon ? "yes" : "no") +
|
|
93
|
-
")",
|
|
91
|
+
"📄 Loaded src/app.html (favicon override: " + (appHtml.hasCustomFavicon ? "yes" : "no") + ")",
|
|
94
92
|
);
|
|
95
93
|
} catch (err) {
|
|
96
94
|
console.error(`❌ src/app.html validation failed:\n${(err as Error).message}`);
|
|
@@ -114,8 +114,7 @@
|
|
|
114
114
|
// We always issue the fetch (even when all loaders skip) so page-level
|
|
115
115
|
// metadata stays fresh on every navigation; only the loaders flagged in
|
|
116
116
|
// the mask actually run server-side.
|
|
117
|
-
const maskBits =
|
|
118
|
-
(pageRun ? "1" : "0") + layoutRunFlags.map((b) => (b ? "1" : "0")).join("");
|
|
117
|
+
const maskBits = (pageRun ? "1" : "0") + layoutRunFlags.map((b) => (b ? "1" : "0")).join("");
|
|
119
118
|
|
|
120
119
|
// Clear dirty set now — we've baked it into the mask.
|
|
121
120
|
clearDirty();
|
|
@@ -173,9 +172,7 @@
|
|
|
173
172
|
? errInfo
|
|
174
173
|
: "Internal Server Error";
|
|
175
174
|
const errDepth: number =
|
|
176
|
-
typeof result?.errorDepth === "number"
|
|
177
|
-
? result.errorDepth
|
|
178
|
-
: match.route.layouts.length;
|
|
175
|
+
typeof result?.errorDepth === "number" ? result.errorDepth : match.route.layouts.length;
|
|
179
176
|
const errOrigin = result?.errorOrigin === "layout" ? "layout" : "page";
|
|
180
177
|
const picked = pickErrorPage(match.route.errorPages ?? [], errDepth, errOrigin);
|
|
181
178
|
if (!picked) {
|
|
@@ -309,9 +306,7 @@
|
|
|
309
306
|
if (result?.metadata) {
|
|
310
307
|
if (result.metadata.title) document.title = result.metadata.title;
|
|
311
308
|
if (result.metadata.description) {
|
|
312
|
-
let meta = document.querySelector(
|
|
313
|
-
'meta[name="description"]',
|
|
314
|
-
) as HTMLMetaElement | null;
|
|
309
|
+
let meta = document.querySelector('meta[name="description"]') as HTMLMetaElement | null;
|
|
315
310
|
if (!meta) {
|
|
316
311
|
meta = document.createElement("meta");
|
|
317
312
|
meta.name = "description";
|
|
@@ -57,10 +57,7 @@ export const router = new (class Router {
|
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
// Canonicalize trailing slash before navigating (matches server 308 behavior)
|
|
60
|
-
const canonical = canonicalPathname(
|
|
61
|
-
pathname,
|
|
62
|
-
(match.route as any).trailingSlash ?? "never",
|
|
63
|
-
);
|
|
60
|
+
const canonical = canonicalPathname(pathname, (match.route as any).trailingSlash ?? "never");
|
|
64
61
|
const finalPath = canonical !== null ? canonical + queryHash : path;
|
|
65
62
|
|
|
66
63
|
const navType: NavType = opts.source ?? "link";
|
|
@@ -108,8 +105,7 @@ export const router = new (class Router {
|
|
|
108
105
|
// Same-page hash navigation: skip page reload, just update URL and scroll
|
|
109
106
|
// to the target element. Mirrors browser default for in-page anchors.
|
|
110
107
|
const samePage =
|
|
111
|
-
anchor.pathname === window.location.pathname &&
|
|
112
|
-
anchor.search === window.location.search;
|
|
108
|
+
anchor.pathname === window.location.pathname && anchor.search === window.location.search;
|
|
113
109
|
if (samePage && anchor.hash) {
|
|
114
110
|
e.preventDefault();
|
|
115
111
|
const finalPath = anchor.pathname + anchor.search + anchor.hash;
|
|
@@ -127,8 +123,7 @@ export const router = new (class Router {
|
|
|
127
123
|
|
|
128
124
|
// Browser back/forward
|
|
129
125
|
window.addEventListener("popstate", () => {
|
|
130
|
-
const finalPath =
|
|
131
|
-
window.location.pathname + window.location.search + window.location.hash;
|
|
126
|
+
const finalPath = window.location.pathname + window.location.search + window.location.hash;
|
|
132
127
|
// Fire beforeNavigate listeners; popstate can't be reliably cancelled
|
|
133
128
|
// (browser history already advanced), so we surface the event for
|
|
134
129
|
// observation only — `cancel()` is a no-op for this source.
|
package/src/core/config.ts
CHANGED
|
@@ -70,10 +70,7 @@ export async function loadBosiaConfig(cwd: string = process.cwd()): Promise<Bosi
|
|
|
70
70
|
// the project's own node_modules. /tmp would have no node_modules to walk into.
|
|
71
71
|
const cacheDir = join(cwd, ".bosia");
|
|
72
72
|
mkdirSync(cacheDir, { recursive: true });
|
|
73
|
-
const tmpFile = join(
|
|
74
|
-
cacheDir,
|
|
75
|
-
`config.${Date.now()}.${Math.random().toString(36).slice(2)}.mjs`,
|
|
76
|
-
);
|
|
73
|
+
const tmpFile = join(cacheDir, `config.${Date.now()}.${Math.random().toString(36).slice(2)}.mjs`);
|
|
77
74
|
await Bun.write(tmpFile, code);
|
|
78
75
|
|
|
79
76
|
let mod: { default?: BosiaConfig };
|
package/src/core/cookies.ts
CHANGED
|
@@ -98,8 +98,7 @@ export class CookieJar implements Cookies {
|
|
|
98
98
|
}
|
|
99
99
|
let header = `${name}=${encodeURIComponent(value)}`;
|
|
100
100
|
if (opts.path) {
|
|
101
|
-
if (UNSAFE_COOKIE_VALUE.test(opts.path))
|
|
102
|
-
throw new Error(`Invalid cookie path: ${opts.path}`);
|
|
101
|
+
if (UNSAFE_COOKIE_VALUE.test(opts.path)) throw new Error(`Invalid cookie path: ${opts.path}`);
|
|
103
102
|
header += `; Path=${opts.path}`;
|
|
104
103
|
}
|
|
105
104
|
if (opts.domain) {
|
package/src/core/dev-500.ts
CHANGED
package/src/core/html.ts
CHANGED
|
@@ -122,8 +122,7 @@ export function buildHtml(
|
|
|
122
122
|
? `window.__BOSIA_PAGE_DEPS__=${safeJsonStringify(pageDeps)};window.__BOSIA_LAYOUT_DEPS__=${safeJsonStringify(layoutDeps ?? [])};`
|
|
123
123
|
: "";
|
|
124
124
|
|
|
125
|
-
const sysScript =
|
|
126
|
-
ssrFlag || depsScript ? `\n <script${n}>${ssrFlag}${depsScript}</script>` : "";
|
|
125
|
+
const sysScript = ssrFlag || depsScript ? `\n <script${n}>${ssrFlag}${depsScript}</script>` : "";
|
|
127
126
|
|
|
128
127
|
const dataIslands = csr
|
|
129
128
|
? `\n <script${n} type="application/json" id="__bosia-page-data__">${safeJsonForScript(pageData)}</script>` +
|
package/src/core/plugin.ts
CHANGED
|
@@ -60,9 +60,7 @@ export function makeBosiaPlugin(target: "browser" | "bun" = "bun") {
|
|
|
60
60
|
let svelteBrowserEntry: string | null = null;
|
|
61
61
|
if (target === "browser") {
|
|
62
62
|
try {
|
|
63
|
-
const svelteDir = dirname(
|
|
64
|
-
require.resolve("svelte/package.json", { paths: [appDir] }),
|
|
65
|
-
);
|
|
63
|
+
const svelteDir = dirname(require.resolve("svelte/package.json", { paths: [appDir] }));
|
|
66
64
|
const pkg = require(join(svelteDir, "package.json"));
|
|
67
65
|
const dotExport = pkg.exports?.["."];
|
|
68
66
|
const browserPath = typeof dotExport === "object" ? dotExport.browser : null;
|
|
@@ -160,10 +160,7 @@ export function createInspectorBunPlugin(opts: InspectorBunPluginOptions): BunPl
|
|
|
160
160
|
// line numbers differ. The resolver translates browser-side stack
|
|
161
161
|
// frames (delivered via SSE), which run client code.
|
|
162
162
|
if (dev && generate === "client" && result.js.map) {
|
|
163
|
-
const m =
|
|
164
|
-
typeof result.js.map === "string"
|
|
165
|
-
? JSON.parse(result.js.map)
|
|
166
|
-
: result.js.map;
|
|
163
|
+
const m = typeof result.js.map === "string" ? JSON.parse(result.js.map) : result.js.map;
|
|
167
164
|
svelteMapCache.set(args.path, m);
|
|
168
165
|
}
|
|
169
166
|
|
|
@@ -174,9 +174,7 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
174
174
|
name: "inspector",
|
|
175
175
|
|
|
176
176
|
build: {
|
|
177
|
-
bunPlugins: (target) => [
|
|
178
|
-
createInspectorBunPlugin({ cwd: process.cwd(), target, dev: true }),
|
|
179
|
-
],
|
|
177
|
+
bunPlugins: (target) => [createInspectorBunPlugin({ cwd: process.cwd(), target, dev: true })],
|
|
180
178
|
},
|
|
181
179
|
|
|
182
180
|
backend: {
|
|
@@ -195,13 +193,10 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
195
193
|
const line = Number.isFinite(data.line) ? Number(data.line) : null;
|
|
196
194
|
const col = Number.isFinite(data.col) ? Number(data.col) : 1;
|
|
197
195
|
if (!file || line === null) {
|
|
198
|
-
return new Response(
|
|
199
|
-
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
headers: { "content-type": "application/json" },
|
|
203
|
-
},
|
|
204
|
-
);
|
|
196
|
+
return new Response(JSON.stringify({ ok: false, error: "missing file/line" }), {
|
|
197
|
+
status: 400,
|
|
198
|
+
headers: { "content-type": "application/json" },
|
|
199
|
+
});
|
|
205
200
|
}
|
|
206
201
|
|
|
207
202
|
const comment = typeof data.comment === "string" ? data.comment.trim() : "";
|
|
@@ -245,13 +240,10 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
245
240
|
return { ok: true, mode: "ai" as const };
|
|
246
241
|
} catch (err) {
|
|
247
242
|
console.error("[inspector] aiEndpoint POST failed:", err);
|
|
248
|
-
return new Response(
|
|
249
|
-
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
headers: { "content-type": "application/json" },
|
|
253
|
-
},
|
|
254
|
-
);
|
|
243
|
+
return new Response(JSON.stringify({ ok: false, error: "ai endpoint failed" }), {
|
|
244
|
+
status: 502,
|
|
245
|
+
headers: { "content-type": "application/json" },
|
|
246
|
+
});
|
|
255
247
|
}
|
|
256
248
|
}
|
|
257
249
|
|
|
@@ -266,13 +258,10 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
266
258
|
});
|
|
267
259
|
} catch (err) {
|
|
268
260
|
console.error(`[inspector] failed to launch "${editor}":`, err);
|
|
269
|
-
return new Response(
|
|
270
|
-
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
headers: { "content-type": "application/json" },
|
|
274
|
-
},
|
|
275
|
-
);
|
|
261
|
+
return new Response(JSON.stringify({ ok: false, error: "editor launch failed" }), {
|
|
262
|
+
status: 500,
|
|
263
|
+
headers: { "content-type": "application/json" },
|
|
264
|
+
});
|
|
276
265
|
}
|
|
277
266
|
return { ok: true, mode: "editor" as const };
|
|
278
267
|
});
|
|
@@ -297,46 +286,43 @@ export function inspector(options: InspectorOptions = {}): BosiaPlugin | false {
|
|
|
297
286
|
// Live SSE stream. New clients also get a flush of the bounded
|
|
298
287
|
// replay buffer so errors that fired during a failing render
|
|
299
288
|
// (before the 500 page's overlay could subscribe) are visible.
|
|
300
|
-
chained = chained.get(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
289
|
+
chained = chained.get("/__bosia/errors", ({ request }: { request: Request }) => {
|
|
290
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
291
|
+
start(ctrl) {
|
|
292
|
+
sseClients.add(ctrl);
|
|
293
|
+
try {
|
|
294
|
+
ctrl.enqueue(encode(":ok\n\n"));
|
|
295
|
+
} catch {
|
|
296
|
+
sseClients.delete(ctrl);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
flushReplay(ctrl);
|
|
300
|
+
const ping = setInterval(() => {
|
|
306
301
|
try {
|
|
307
|
-
ctrl.enqueue(encode(":
|
|
302
|
+
ctrl.enqueue(encode(":ping\n\n"));
|
|
308
303
|
} catch {
|
|
304
|
+
clearInterval(ping);
|
|
309
305
|
sseClients.delete(ctrl);
|
|
310
|
-
return;
|
|
311
306
|
}
|
|
312
|
-
|
|
313
|
-
const ping = setInterval(() => {
|
|
314
|
-
try {
|
|
315
|
-
ctrl.enqueue(encode(":ping\n\n"));
|
|
316
|
-
} catch {
|
|
317
|
-
clearInterval(ping);
|
|
318
|
-
sseClients.delete(ctrl);
|
|
319
|
-
}
|
|
320
|
-
}, 25_000);
|
|
307
|
+
}, 25_000);
|
|
321
308
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
) as unknown as Elysia;
|
|
309
|
+
request.signal.addEventListener("abort", () => {
|
|
310
|
+
clearInterval(ping);
|
|
311
|
+
sseClients.delete(ctrl);
|
|
312
|
+
try {
|
|
313
|
+
ctrl.close();
|
|
314
|
+
} catch {}
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
return new Response(stream, {
|
|
319
|
+
headers: {
|
|
320
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
321
|
+
"Cache-Control": "no-cache",
|
|
322
|
+
Connection: "keep-alive",
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}) as unknown as Elysia;
|
|
340
326
|
}
|
|
341
327
|
|
|
342
328
|
return chained;
|
package/src/core/renderer.ts
CHANGED
|
@@ -148,8 +148,7 @@ function makeFetch(req: Request, url: URL) {
|
|
|
148
148
|
|
|
149
149
|
const headers = new Headers(init?.headers);
|
|
150
150
|
const trusted =
|
|
151
|
-
targetOrigin !== null &&
|
|
152
|
-
(targetOrigin === sameOrigin || INTERNAL_HOSTS.has(targetOrigin));
|
|
151
|
+
targetOrigin !== null && (targetOrigin === sameOrigin || INTERNAL_HOSTS.has(targetOrigin));
|
|
153
152
|
if (cookie && trusted && !headers.has("cookie")) headers.set("cookie", cookie);
|
|
154
153
|
|
|
155
154
|
return globalThis.fetch(resolved, { ...init, headers });
|
|
@@ -927,9 +926,7 @@ export async function renderErrorPage(
|
|
|
927
926
|
const K = picked.depth;
|
|
928
927
|
const [errorMod, layoutMods] = await Promise.all([
|
|
929
928
|
picked.loader(),
|
|
930
|
-
Promise.all(
|
|
931
|
-
route.layoutModules.slice(0, K).map((l: () => Promise<any>) => l()),
|
|
932
|
-
),
|
|
929
|
+
Promise.all(route.layoutModules.slice(0, K).map((l: () => Promise<any>) => l())),
|
|
933
930
|
]);
|
|
934
931
|
const layoutData: Record<string, any>[] = [];
|
|
935
932
|
for (let i = 0; i < K; i++) layoutData.push(partialLayoutData?.[i] ?? {});
|
|
@@ -962,11 +959,7 @@ export async function renderErrorPage(
|
|
|
962
959
|
return compress(html, "text/html; charset=utf-8", req, status);
|
|
963
960
|
} catch (err) {
|
|
964
961
|
if (isDev) console.error("Nested error page render failed:", err);
|
|
965
|
-
else
|
|
966
|
-
console.error(
|
|
967
|
-
"Nested error page render failed:",
|
|
968
|
-
(err as Error).message ?? err,
|
|
969
|
-
);
|
|
962
|
+
else console.error("Nested error page render failed:", (err as Error).message ?? err);
|
|
970
963
|
if (isDev) reportDevErrorFromCatch(err);
|
|
971
964
|
// fall through to global / text fallback
|
|
972
965
|
}
|
package/src/core/routeTypes.ts
CHANGED
|
@@ -78,9 +78,7 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
78
78
|
params.length === 0 ? `{}` : `{ ${params.map((p) => `${p}: string`).join("; ")} }`;
|
|
79
79
|
|
|
80
80
|
const lines: string[] = ["// AUTO-GENERATED by bosia — do not edit\n"];
|
|
81
|
-
lines.push(
|
|
82
|
-
`import type { LoadEvent, MetadataEvent, RequestEvent, Metadata } from 'bosia';`,
|
|
83
|
-
);
|
|
81
|
+
lines.push(`import type { LoadEvent, MetadataEvent, RequestEvent, Metadata } from 'bosia';`);
|
|
84
82
|
lines.push(``);
|
|
85
83
|
lines.push(`export type Params = ${paramsType};`);
|
|
86
84
|
lines.push(``);
|
|
@@ -126,9 +124,7 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
126
124
|
lines.push(
|
|
127
125
|
`type _ActionReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;`,
|
|
128
126
|
);
|
|
129
|
-
lines.push(
|
|
130
|
-
`type _UnwrapFailure<T> = T extends { status: number; data: infer D } ? D : T;`,
|
|
131
|
-
);
|
|
127
|
+
lines.push(`type _UnwrapFailure<T> = T extends { status: number; data: infer D } ? D : T;`);
|
|
132
128
|
lines.push(
|
|
133
129
|
`export type ActionData = _actions extends Record<string, (...args: any[]) => any>`,
|
|
134
130
|
);
|
|
@@ -140,9 +136,7 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
140
136
|
lines.push(`\nimport type { load as _layoutLoad } from '${srcBase}+layout.server.ts';`);
|
|
141
137
|
lines.push(`export type LayoutServerLoad = (event: _LoadEvent) => any;`);
|
|
142
138
|
lines.push(`export type LayoutData = Awaited<ReturnType<typeof _layoutLoad>>;`);
|
|
143
|
-
lines.push(
|
|
144
|
-
`export type LayoutProps = { data: LayoutData; params: Params; children: any };`,
|
|
145
|
-
);
|
|
139
|
+
lines.push(`export type LayoutProps = { data: LayoutData; params: Params; children: any };`);
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
const outDir = join(process.cwd(), ".bosia", "types", "src", "routes", ...segments);
|