bosia 0.6.20 → 0.6.22
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 +5 -5
- package/src/cli/addRouter.ts +53 -0
- package/src/cli/block.ts +19 -12
- package/src/cli/create.ts +6 -11
- package/src/cli/feat.ts +19 -22
- package/src/cli/index.ts +25 -23
- package/src/cli/manifest.ts +1 -1
- package/src/cli/registry.ts +40 -2
- package/src/core/build.ts +1 -3
- package/src/core/client/App.svelte +3 -8
- package/src/core/client/page.svelte.ts +28 -0
- 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 +9 -34
- package/src/core/staticManifest.ts +1 -3
- package/src/core/svelteAudit.ts +2 -5
- package/src/core/svelteCompiler.ts +2 -8
- package/src/lib/client.ts +1 -0
- package/templates/default/.prettierignore +1 -0
- package/templates/demo/.prettierignore +1 -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 +27 -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 +132 -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 +14 -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 +30 -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/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.22",
|
|
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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resolveLocalRegistryOrExit,
|
|
8
8
|
readRegistryJSON,
|
|
9
9
|
readRegistryFile,
|
|
10
|
+
writeRegistryFile,
|
|
10
11
|
mergePkgJson,
|
|
11
12
|
bunAdd,
|
|
12
13
|
} from "./registry.ts";
|
|
@@ -150,7 +151,7 @@ export async function addComponent(name: string, root = false, options?: Install
|
|
|
150
151
|
const content = await readRegistryFile(registryRoot, "components", fullPath, file);
|
|
151
152
|
const dest = join(destDir, file);
|
|
152
153
|
if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
|
|
153
|
-
|
|
154
|
+
writeRegistryFile(dest, content);
|
|
154
155
|
console.log(` ✍️ src/lib/components/${fullPath}/${file}`);
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -158,8 +159,7 @@ export async function addComponent(name: string, root = false, options?: Install
|
|
|
158
159
|
if (Object.keys(meta.npmDeps).length > 0) {
|
|
159
160
|
if (options?.skipInstall) {
|
|
160
161
|
const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
|
|
161
|
-
if (addedDeps.length > 0)
|
|
162
|
-
console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
162
|
+
if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
163
163
|
} else {
|
|
164
164
|
await bunAdd(cwd, meta.npmDeps);
|
|
165
165
|
}
|
|
@@ -238,8 +238,8 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
238
238
|
}
|
|
239
239
|
`;
|
|
240
240
|
|
|
241
|
-
export function ensureUtils() {
|
|
242
|
-
const utilsPath = join(
|
|
241
|
+
export function ensureUtils(cwd: string = process.cwd()) {
|
|
242
|
+
const utilsPath = join(cwd, "src", "lib", "utils.ts");
|
|
243
243
|
if (!existsSync(utilsPath)) {
|
|
244
244
|
mkdirSync(dirname(utilsPath), { recursive: true });
|
|
245
245
|
writeFileSync(utilsPath, UTILS_CONTENT, "utf-8");
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// ─── Dispatch logic for `bosia add ...` ──────────────────
|
|
2
|
+
// Split out from index.ts so the routing can be unit-tested with injected
|
|
3
|
+
// runners (index.ts does top-level `process.argv` parsing on import).
|
|
4
|
+
|
|
5
|
+
export interface AddRunners {
|
|
6
|
+
runAdd: (names: string[], flags: string[]) => Promise<void> | void;
|
|
7
|
+
runAddBlock: (name: string | undefined, flags: string[]) => Promise<void> | void;
|
|
8
|
+
runAddTheme: (name: string | undefined, flags: string[]) => Promise<void> | void;
|
|
9
|
+
runAddFont: (family: string | undefined, url: string | undefined) => Promise<void> | void;
|
|
10
|
+
runAddList: () => Promise<void> | void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function routeAdd(args: string[], runners: AddRunners): Promise<void> {
|
|
14
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
15
|
+
const flags = args.filter((a) => a.startsWith("-"));
|
|
16
|
+
const sub = positional[0];
|
|
17
|
+
|
|
18
|
+
if (sub === "block") {
|
|
19
|
+
await runners.runAddBlock(positional[1], flags);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (sub === "theme") {
|
|
23
|
+
const themeFlags = args.filter((a) => a.startsWith("--"));
|
|
24
|
+
await runners.runAddTheme(positional[1], themeFlags);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (sub === "font") {
|
|
28
|
+
await runners.runAddFont(positional[1], positional[2]);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (sub === "list") {
|
|
32
|
+
await runners.runAddList();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Alias: `blocks/<cat>/<name>` tokens dispatch to runAddBlock.
|
|
37
|
+
// Skills/AI agents frequently emit the plural `blocks/...` form alongside
|
|
38
|
+
// `ui/*` components; route those to the block installer transparently
|
|
39
|
+
// and let any remaining plain component names fall through to runAdd.
|
|
40
|
+
const blockTokens = positional.filter((p) => p.startsWith("blocks/"));
|
|
41
|
+
const componentTokens = positional.filter((p) => !p.startsWith("blocks/"));
|
|
42
|
+
if (blockTokens.length > 0) {
|
|
43
|
+
if (componentTokens.length > 0) {
|
|
44
|
+
await runners.runAdd(componentTokens, flags);
|
|
45
|
+
}
|
|
46
|
+
for (const token of blockTokens) {
|
|
47
|
+
await runners.runAddBlock(token.slice("blocks/".length), flags);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await runners.runAdd(positional, flags);
|
|
53
|
+
}
|
package/src/cli/block.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { join, dirname } from "path";
|
|
2
|
-
import { mkdirSync,
|
|
2
|
+
import { mkdirSync, existsSync } from "fs";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import {
|
|
5
5
|
type InstallOptions,
|
|
6
6
|
resolveLocalRegistryOrExit,
|
|
7
7
|
readRegistryJSON,
|
|
8
8
|
readRegistryFile,
|
|
9
|
+
writeRegistryFile,
|
|
9
10
|
mergePkgJson,
|
|
10
11
|
bunAdd,
|
|
11
12
|
} from "./registry.ts";
|
|
@@ -43,24 +44,33 @@ export async function runAddBlock(
|
|
|
43
44
|
|
|
44
45
|
const local = flags.includes("--local");
|
|
45
46
|
const flagYes = flags.includes("-y") || flags.includes("--yes");
|
|
46
|
-
|
|
47
|
-
|
|
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`);
|
|
48
51
|
|
|
49
52
|
const resolvedOptions: InstallOptions = {
|
|
50
53
|
...(options ?? {}),
|
|
54
|
+
registryRoot,
|
|
51
55
|
skipPrompts: options?.skipPrompts ?? flagYes,
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
await initAddRegistry(registryRoot);
|
|
55
|
-
ensureUtils();
|
|
59
|
+
ensureUtils(resolvedOptions.cwd);
|
|
56
60
|
|
|
57
61
|
console.log(`⬡ Installing block: ${name}\n`);
|
|
58
62
|
|
|
59
63
|
const meta = await readRegistryJSON<BlockMeta>(registryRoot, "blocks", name, "meta.json");
|
|
60
64
|
|
|
61
|
-
// 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.
|
|
62
68
|
for (const dep of meta.dependencies ?? []) {
|
|
63
|
-
|
|
69
|
+
if (dep.startsWith("blocks/")) {
|
|
70
|
+
await runAddBlock(dep.slice("blocks/".length), [], resolvedOptions);
|
|
71
|
+
} else {
|
|
72
|
+
await addComponent(dep, false, resolvedOptions);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
// 2. Copy block files to src/lib/blocks/<path>/
|
|
@@ -83,7 +93,7 @@ export async function runAddBlock(
|
|
|
83
93
|
const content = await readRegistryFile(registryRoot, "blocks", name, file);
|
|
84
94
|
const dest = join(destDir, file);
|
|
85
95
|
if (file.includes("/")) mkdirSync(dirname(dest), { recursive: true });
|
|
86
|
-
|
|
96
|
+
writeRegistryFile(dest, content);
|
|
87
97
|
console.log(` ✍️ src/lib/blocks/${name}/${file}`);
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -102,8 +112,7 @@ export async function runAddBlock(
|
|
|
102
112
|
if (meta.npmDeps && Object.keys(meta.npmDeps).length > 0) {
|
|
103
113
|
if (resolvedOptions.skipInstall) {
|
|
104
114
|
const { addedDeps } = mergePkgJson(cwd, { deps: meta.npmDeps });
|
|
105
|
-
if (addedDeps.length > 0)
|
|
106
|
-
console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
115
|
+
if (addedDeps.length > 0) console.log(` 📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
107
116
|
} else {
|
|
108
117
|
await bunAdd(cwd, meta.npmDeps);
|
|
109
118
|
}
|
|
@@ -118,9 +127,7 @@ export async function runAddBlock(
|
|
|
118
127
|
...(meta.dependencies && meta.dependencies.length > 0
|
|
119
128
|
? { dependencies: meta.dependencies }
|
|
120
129
|
: {}),
|
|
121
|
-
...(meta.fonts && Object.keys(meta.fonts).length > 0
|
|
122
|
-
? { fonts: Object.keys(meta.fonts) }
|
|
123
|
-
: {}),
|
|
130
|
+
...(meta.fonts && Object.keys(meta.fonts).length > 0 ? { fonts: Object.keys(meta.fonts) } : {}),
|
|
124
131
|
});
|
|
125
132
|
|
|
126
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
|
@@ -59,27 +59,29 @@ async function main() {
|
|
|
59
59
|
break;
|
|
60
60
|
}
|
|
61
61
|
case "add": {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
const { routeAdd } = await import("./addRouter.ts");
|
|
63
|
+
await routeAdd(args, {
|
|
64
|
+
runAdd: async (names, flags) => {
|
|
65
|
+
const { runAdd } = await import("./add.ts");
|
|
66
|
+
await runAdd(names, flags);
|
|
67
|
+
},
|
|
68
|
+
runAddBlock: async (name, flags) => {
|
|
69
|
+
const { runAddBlock } = await import("./block.ts");
|
|
70
|
+
await runAddBlock(name, flags);
|
|
71
|
+
},
|
|
72
|
+
runAddTheme: async (name, flags) => {
|
|
73
|
+
const { runAddTheme } = await import("./theme.ts");
|
|
74
|
+
await runAddTheme(name, flags);
|
|
75
|
+
},
|
|
76
|
+
runAddFont: async (family, url) => {
|
|
77
|
+
const { runAddFont } = await import("./font.ts");
|
|
78
|
+
await runAddFont(family, url);
|
|
79
|
+
},
|
|
80
|
+
runAddList: async () => {
|
|
81
|
+
const { runAddList } = await import("./add.ts");
|
|
82
|
+
runAddList();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
83
85
|
break;
|
|
84
86
|
}
|
|
85
87
|
case "feat": {
|
|
@@ -94,8 +96,7 @@ async function main() {
|
|
|
94
96
|
// First non-flag token is the feature name; everything else flows through to the
|
|
95
97
|
// feature's own option parser. Global flags (-y, --local) are also accepted here
|
|
96
98
|
// and get split out inside runFeat.
|
|
97
|
-
const rest =
|
|
98
|
-
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)];
|
|
99
100
|
await runFeat(featName, rest);
|
|
100
101
|
break;
|
|
101
102
|
}
|
|
@@ -137,6 +138,7 @@ Examples:
|
|
|
137
138
|
bun x bosia@latest add -y button card → auto-confirm overwrites (CI / scripts)
|
|
138
139
|
bun x bosia@latest add shop/cart → src/lib/components/shop/cart/
|
|
139
140
|
bun x bosia@latest add block cards/feature-editorial
|
|
141
|
+
bun x bosia@latest add blocks/cards/feature-editorial (alias for: add block cards/feature-editorial)
|
|
140
142
|
bun x bosia@latest add theme editorial
|
|
141
143
|
bun x bosia@latest add font "Fredoka" "https://fonts.googleapis.com/css2?family=Fredoka:wght@400;700&display=swap"
|
|
142
144
|
bun x bosia@latest feat login
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join, dirname } from "path";
|
|
2
|
-
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync } from "fs";
|
|
3
3
|
import { spawn } from "bun";
|
|
4
4
|
|
|
5
5
|
// ─── Shared registry utilities for feat.ts and add.ts ─────
|
|
@@ -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. */
|
|
@@ -80,6 +82,42 @@ export async function readRegistryFile(
|
|
|
80
82
|
return fetchText(`${REGISTRY_URL}/${category}/${name}/${file}`);
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
// ─── Registry file writer ─────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write a registry file with one EACCES/EPERM recovery attempt.
|
|
89
|
+
*
|
|
90
|
+
* Component/block file loops abort mid-install when a target path is owned by a
|
|
91
|
+
* different uid — bosapi tenant apps run sandboxed as `bosapi-app-N`, so a
|
|
92
|
+
* subsequent install from the bosapi user hits EACCES on the first foreign-owned
|
|
93
|
+
* file and leaves a partial install behind. Unlink + retry recovers; only the
|
|
94
|
+
* unrecoverable case surfaces the chown hint.
|
|
95
|
+
*/
|
|
96
|
+
export function writeRegistryFile(dest: string, content: string): void {
|
|
97
|
+
try {
|
|
98
|
+
writeFileSync(dest, content, "utf-8");
|
|
99
|
+
return;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
102
|
+
if (code !== "EACCES" && code !== "EPERM") throw err;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(dest);
|
|
106
|
+
} catch {
|
|
107
|
+
// ignore — retry will surface the real error
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(dest, content, "utf-8");
|
|
111
|
+
} catch (retry) {
|
|
112
|
+
const e = retry as NodeJS.ErrnoException;
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Cannot write ${dest}: ${e.code}. ` +
|
|
115
|
+
`The existing file is owned by a different user (likely created from inside ` +
|
|
116
|
+
`the app sandbox). Fix from the project root: chown -R $(whoami) src/lib`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
// ─── package.json helpers ─────────────────────────────────
|
|
84
122
|
|
|
85
123
|
export interface PkgDeps {
|
|
@@ -138,7 +176,7 @@ export function mergePkgJson(
|
|
|
138
176
|
}
|
|
139
177
|
|
|
140
178
|
if (changed) {
|
|
141
|
-
writeFileSync(pkgPath, JSON.stringify(pkg, null,
|
|
179
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n", "utf-8");
|
|
142
180
|
}
|
|
143
181
|
|
|
144
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";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ─── Reactive page object ─────────────────────────────────
|
|
2
|
+
// Mirrors what user-facing skills (bosia-page-shell, bosia-seo,
|
|
3
|
+
// bosia-navigation) teach: `import { page } from "bosia/client"` then read
|
|
4
|
+
// `page.url.pathname`. Backed by `router.currentRoute` (`$state` in
|
|
5
|
+
// router.svelte.ts), so the `$derived` URL re-runs on every nav.
|
|
6
|
+
//
|
|
7
|
+
// No `params` getter on purpose — Bosia already passes `params` as a prop to
|
|
8
|
+
// `+page.svelte` / `+layout.svelte` (see App.svelte), mirroring the modern
|
|
9
|
+
// SvelteKit `$app/state` direction. Route components should destructure
|
|
10
|
+
// `params` from `$props()`, not from here.
|
|
11
|
+
|
|
12
|
+
import { router } from "./router.svelte.ts";
|
|
13
|
+
|
|
14
|
+
class Page {
|
|
15
|
+
#url = $derived.by(() => {
|
|
16
|
+
if (typeof window === "undefined") return new URL("http://localhost/");
|
|
17
|
+
return new URL(router.currentRoute, window.location.origin);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
get url() {
|
|
21
|
+
return this.#url;
|
|
22
|
+
}
|
|
23
|
+
get pathname() {
|
|
24
|
+
return this.#url.pathname;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const page = new Page();
|
|
@@ -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;
|