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.
- package/README.md +39 -39
- package/package.json +56 -53
- package/src/ambient.d.ts +31 -0
- package/src/cli/add.ts +120 -114
- package/src/cli/build.ts +10 -10
- package/src/cli/create.ts +142 -137
- package/src/cli/dev.ts +8 -8
- package/src/cli/feat.ts +291 -132
- package/src/cli/index.ts +51 -42
- package/src/cli/registry.ts +136 -115
- package/src/cli/start.ts +17 -17
- package/src/cli/test.ts +25 -0
- package/src/core/build.ts +72 -56
- package/src/core/client/App.svelte +177 -153
- package/src/core/client/appState.svelte.ts +57 -0
- package/src/core/client/enhance.ts +112 -0
- package/src/core/client/hydrate.ts +97 -65
- package/src/core/client/prefetch.ts +101 -94
- package/src/core/client/router.svelte.ts +64 -51
- package/src/core/cookies.ts +70 -66
- package/src/core/cors.ts +44 -35
- package/src/core/csrf.ts +38 -38
- package/src/core/dedup.ts +17 -17
- package/src/core/dev.ts +165 -168
- package/src/core/env.ts +155 -128
- package/src/core/envCodegen.ts +73 -73
- package/src/core/errors.ts +48 -49
- package/src/core/hooks.ts +50 -50
- package/src/core/html.ts +192 -139
- package/src/core/matcher.ts +130 -121
- package/src/core/paths.ts +8 -10
- package/src/core/plugin.ts +113 -107
- package/src/core/prerender.ts +191 -118
- package/src/core/renderer.ts +359 -265
- package/src/core/routeFile.ts +140 -127
- package/src/core/routeTypes.ts +144 -83
- package/src/core/scanner.ts +125 -95
- package/src/core/server.ts +543 -370
- package/src/core/types.ts +25 -20
- package/src/lib/client.ts +12 -0
- package/src/lib/index.ts +8 -8
- package/src/lib/utils.ts +44 -30
- package/templates/default/.prettierignore +5 -0
- package/templates/default/.prettierrc.json +9 -0
- package/templates/default/README.md +5 -5
- package/templates/default/package.json +22 -18
- package/templates/default/src/app.css +80 -80
- package/templates/default/src/app.d.ts +3 -3
- package/templates/default/src/routes/+error.svelte +7 -10
- package/templates/default/src/routes/+layout.svelte +2 -2
- package/templates/default/src/routes/+page.svelte +31 -29
- package/templates/default/src/routes/about/+page.svelte +3 -3
- package/templates/default/tsconfig.json +20 -20
- package/templates/demo/.prettierignore +5 -0
- package/templates/demo/.prettierrc.json +9 -0
- package/templates/demo/README.md +9 -9
- package/templates/demo/package.json +22 -17
- package/templates/demo/src/app.css +80 -80
- package/templates/demo/src/app.d.ts +3 -3
- package/templates/demo/src/hooks.server.ts +9 -9
- package/templates/demo/src/routes/(public)/+layout.svelte +45 -23
- package/templates/demo/src/routes/(public)/+page.svelte +96 -67
- package/templates/demo/src/routes/(public)/about/+page.svelte +13 -25
- package/templates/demo/src/routes/(public)/all/[...catchall]/+page.svelte +24 -28
- package/templates/demo/src/routes/(public)/blog/+page.svelte +55 -46
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.server.ts +36 -38
- package/templates/demo/src/routes/(public)/blog/[slug]/+page.svelte +60 -42
- package/templates/demo/src/routes/+error.svelte +10 -7
- package/templates/demo/src/routes/+layout.server.ts +4 -4
- package/templates/demo/src/routes/+layout.svelte +2 -2
- package/templates/demo/src/routes/actions-test/+page.server.ts +16 -16
- package/templates/demo/src/routes/actions-test/+page.svelte +49 -49
- package/templates/demo/src/routes/api/hello/+server.ts +25 -25
- package/templates/demo/tsconfig.json +20 -20
- package/templates/todo/.prettierignore +5 -0
- package/templates/todo/.prettierrc.json +9 -0
- package/templates/todo/README.md +9 -9
- package/templates/todo/package.json +22 -17
- package/templates/todo/src/app.css +80 -80
- package/templates/todo/src/app.d.ts +7 -7
- package/templates/todo/src/hooks.server.ts +9 -9
- package/templates/todo/src/routes/+error.svelte +10 -7
- package/templates/todo/src/routes/+layout.server.ts +4 -4
- package/templates/todo/src/routes/+layout.svelte +2 -2
- package/templates/todo/src/routes/+page.svelte +44 -44
- package/templates/todo/template.json +1 -1
- package/templates/todo/tsconfig.json +20 -20
package/src/cli/feat.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { join, dirname } from "path";
|
|
1
|
+
import { join, dirname, extname } from "path";
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import { addComponent, initAddRegistry } from "./add.ts";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
type InstallOptions,
|
|
7
|
+
resolveLocalRegistryOrExit,
|
|
8
|
+
readRegistryJSON,
|
|
9
|
+
readRegistryFile,
|
|
10
|
+
mergePkgJson,
|
|
11
|
+
bunAdd,
|
|
12
12
|
} from "./registry.ts";
|
|
13
13
|
|
|
14
14
|
// ─── bosia feat <feature> [--local] ──────────────────────
|
|
@@ -16,17 +16,30 @@ import {
|
|
|
16
16
|
// registry with --local) and copies route/lib files, installs npm deps.
|
|
17
17
|
// Supports nested feature dependencies (e.g. todo → drizzle).
|
|
18
18
|
|
|
19
|
+
type FileStrategy =
|
|
20
|
+
| "write" // overwrite (prompt if interactive)
|
|
21
|
+
| "skip-if-exists" // bootstrap-once: never replace user copy
|
|
22
|
+
| "append-line" // idempotent line append (barrel re-exports)
|
|
23
|
+
| "append-block" // marker-delimited block, replaced by id on re-install
|
|
24
|
+
| "merge-json"; // deep-merge JSON, preserve existing keys
|
|
25
|
+
|
|
26
|
+
interface FileEntry {
|
|
27
|
+
src: string;
|
|
28
|
+
target: string;
|
|
29
|
+
strategy?: FileStrategy;
|
|
30
|
+
marker?: string; // unique id within target (default = feature name)
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
interface FeatureMeta {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
envVars?: Record<string, string>; // env vars to append to .env if missing
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
features?: string[]; // other bosia features required
|
|
37
|
+
components: string[]; // bosia components to install via `bosia add`
|
|
38
|
+
files: FileEntry[]; // file entries with per-file strategy
|
|
39
|
+
npmDeps: Record<string, string>;
|
|
40
|
+
npmDevDeps?: Record<string, string>;
|
|
41
|
+
scripts?: Record<string, string>; // package.json scripts to add
|
|
42
|
+
envVars?: Record<string, string>; // env vars to append to .env if missing
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
let registryRoot: string | null = null;
|
|
@@ -35,131 +48,277 @@ let registryRoot: string | null = null;
|
|
|
35
48
|
const installedFeats = new Set<string>();
|
|
36
49
|
|
|
37
50
|
export async function runFeat(name: string | undefined, flags: string[] = []) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
if (!name) {
|
|
52
|
+
console.error(
|
|
53
|
+
"❌ Please provide a feature name.\n Usage: bosia feat <feature> [--local]",
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
if (flags.includes("--local")) {
|
|
59
|
+
registryRoot = resolveLocalRegistryOrExit();
|
|
60
|
+
console.log(`⬡ Using local registry: ${registryRoot}\n`);
|
|
61
|
+
}
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
// Initialize add.ts registry context so addComponent resolves paths correctly
|
|
64
|
+
await initAddRegistry(registryRoot);
|
|
50
65
|
|
|
51
|
-
|
|
66
|
+
await installFeature(name, true);
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/** Set the registry root for feature resolution. Called by create.ts for template features. */
|
|
55
70
|
export function initFeatRegistry(root: string | null) {
|
|
56
|
-
|
|
71
|
+
registryRoot = root;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
export async function installFeature(name: string, isRoot: boolean, options?: InstallOptions) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
75
|
+
if (installedFeats.has(name)) return;
|
|
76
|
+
installedFeats.add(name);
|
|
77
|
+
|
|
78
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
isRoot ? `⬡ Installing feature: ${name}\n` : `\n⬡ Installing dependency feature: ${name}\n`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const meta = await readRegistryJSON<FeatureMeta>(registryRoot, "features", name, "meta.json");
|
|
85
|
+
|
|
86
|
+
// Install required feature dependencies first (recursive)
|
|
87
|
+
if (meta.features && meta.features.length > 0) {
|
|
88
|
+
for (const feat of meta.features) {
|
|
89
|
+
await installFeature(feat, false, options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Install required UI components
|
|
94
|
+
if (meta.components.length > 0) {
|
|
95
|
+
console.log("📦 Installing required components...");
|
|
96
|
+
for (const comp of meta.components) {
|
|
97
|
+
await addComponent(comp, false, options);
|
|
98
|
+
}
|
|
99
|
+
console.log("");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Apply each file entry per its strategy
|
|
103
|
+
const createdDirs = new Set<string>();
|
|
104
|
+
for (const entry of meta.files) {
|
|
105
|
+
const dest = join(cwd, entry.target);
|
|
106
|
+
const strategy: FileStrategy = entry.strategy ?? "write";
|
|
107
|
+
const dir = dirname(dest);
|
|
108
|
+
if (!createdDirs.has(dir)) {
|
|
109
|
+
mkdirSync(dir, { recursive: true });
|
|
110
|
+
createdDirs.add(dir);
|
|
111
|
+
}
|
|
112
|
+
const content = await readRegistryFile(registryRoot, "features", name, entry.src);
|
|
113
|
+
await applyStrategy({
|
|
114
|
+
dest,
|
|
115
|
+
target: entry.target,
|
|
116
|
+
content,
|
|
117
|
+
strategy,
|
|
118
|
+
feat: name,
|
|
119
|
+
marker: entry.marker ?? name,
|
|
120
|
+
skipPrompts: options?.skipPrompts ?? false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Install npm dependencies
|
|
125
|
+
const hasDeps = Object.keys(meta.npmDeps).length > 0;
|
|
126
|
+
const hasDevDeps = Object.keys(meta.npmDevDeps ?? {}).length > 0;
|
|
127
|
+
const hasScripts = Object.keys(meta.scripts ?? {}).length > 0;
|
|
128
|
+
|
|
129
|
+
if (hasDeps || hasDevDeps) {
|
|
130
|
+
if (options?.skipInstall) {
|
|
131
|
+
const { addedDeps, addedScripts } = mergePkgJson(cwd, {
|
|
132
|
+
deps: meta.npmDeps,
|
|
133
|
+
devDeps: meta.npmDevDeps,
|
|
134
|
+
scripts: meta.scripts,
|
|
135
|
+
});
|
|
136
|
+
if (addedDeps.length > 0)
|
|
137
|
+
console.log(`\n📥 Added to package.json: ${addedDeps.join(", ")}`);
|
|
138
|
+
if (addedScripts.length > 0)
|
|
139
|
+
console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
140
|
+
} else {
|
|
141
|
+
await bunAdd(cwd, meta.npmDeps, meta.npmDevDeps);
|
|
142
|
+
if (hasScripts) {
|
|
143
|
+
const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
|
|
144
|
+
if (addedScripts.length > 0)
|
|
145
|
+
console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else if (hasScripts) {
|
|
149
|
+
const { addedScripts } = mergePkgJson(cwd, { scripts: meta.scripts });
|
|
150
|
+
if (addedScripts.length > 0) console.log(`\n📜 Added scripts: ${addedScripts.join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Append env vars to .env if missing
|
|
154
|
+
const envEntries = Object.entries(meta.envVars ?? {});
|
|
155
|
+
if (envEntries.length > 0) {
|
|
156
|
+
const envPath = join(cwd, ".env");
|
|
157
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
|
|
158
|
+
const toAdd: string[] = [];
|
|
159
|
+
for (const [key, val] of envEntries) {
|
|
160
|
+
if (!existing.includes(`${key}=`)) {
|
|
161
|
+
toAdd.push(`${key}=${val}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (toAdd.length > 0) {
|
|
165
|
+
const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
166
|
+
writeFileSync(envPath, existing + nl + toAdd.join("\n") + "\n", "utf-8");
|
|
167
|
+
console.log(`\n🔑 Added to .env: ${toAdd.map((l) => l.split("=")[0]).join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isRoot) {
|
|
172
|
+
console.log(`\n✅ Feature "${name}" scaffolded!`);
|
|
173
|
+
if (meta.description) console.log(` ${meta.description}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(` ✅ Dependency feature "${name}" installed.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── File strategies ──────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
interface StrategyArgs {
|
|
182
|
+
dest: string;
|
|
183
|
+
target: string;
|
|
184
|
+
content: string;
|
|
185
|
+
strategy: FileStrategy;
|
|
186
|
+
feat: string;
|
|
187
|
+
marker: string;
|
|
188
|
+
skipPrompts: boolean;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function applyStrategy(args: StrategyArgs): Promise<void> {
|
|
192
|
+
const { dest, target, content, strategy, feat, marker, skipPrompts } = args;
|
|
193
|
+
|
|
194
|
+
switch (strategy) {
|
|
195
|
+
case "write": {
|
|
196
|
+
if (existsSync(dest) && !skipPrompts) {
|
|
197
|
+
const replace = await p.confirm({
|
|
198
|
+
message: `File "${target}" already exists. Replace it?`,
|
|
199
|
+
});
|
|
200
|
+
if (p.isCancel(replace) || !replace) {
|
|
201
|
+
console.log(` ⏭️ Skipped ${target}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
writeFileSync(dest, content, "utf-8");
|
|
206
|
+
console.log(` ✍️ ${target}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "skip-if-exists": {
|
|
211
|
+
if (existsSync(dest)) {
|
|
212
|
+
console.log(` ⏭️ Kept existing ${target}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
writeFileSync(dest, content, "utf-8");
|
|
216
|
+
console.log(` ✍️ ${target}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case "append-line": {
|
|
221
|
+
const existing = existsSync(dest) ? readFileSync(dest, "utf-8") : "";
|
|
222
|
+
const existingLines = new Set(
|
|
223
|
+
existing
|
|
224
|
+
.split("\n")
|
|
225
|
+
.map((l) => l.trim())
|
|
226
|
+
.filter(Boolean),
|
|
227
|
+
);
|
|
228
|
+
const newLines = content
|
|
229
|
+
.split("\n")
|
|
230
|
+
.map((l) => l.trim())
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.filter((l) => !existingLines.has(l));
|
|
233
|
+
|
|
234
|
+
if (newLines.length === 0) {
|
|
235
|
+
console.log(` ⏭️ ${target} (no new lines)`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
240
|
+
writeFileSync(dest, existing + nl + newLines.join("\n") + "\n", "utf-8");
|
|
241
|
+
console.log(
|
|
242
|
+
` ➕ ${target} (+${newLines.length} line${newLines.length === 1 ? "" : "s"})`,
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
case "append-block": {
|
|
248
|
+
const id = `bosia:${feat}:${marker}`;
|
|
249
|
+
const delim = blockDelim(extname(dest));
|
|
250
|
+
const startLine = delim.end
|
|
251
|
+
? `${delim.start} >>> ${id} ${delim.end}`
|
|
252
|
+
: `${delim.start} >>> ${id}`;
|
|
253
|
+
const endLine = delim.end
|
|
254
|
+
? `${delim.start} <<< ${id} ${delim.end}`
|
|
255
|
+
: `${delim.start} <<< ${id}`;
|
|
256
|
+
const block = `${startLine}\n${content.trimEnd()}\n${endLine}`;
|
|
257
|
+
|
|
258
|
+
const existing = existsSync(dest) ? readFileSync(dest, "utf-8") : "";
|
|
259
|
+
|
|
260
|
+
if (existing.includes(startLine) && existing.includes(endLine)) {
|
|
261
|
+
const re = new RegExp(`${escapeRegex(startLine)}[\\s\\S]*?${escapeRegex(endLine)}`);
|
|
262
|
+
writeFileSync(dest, existing.replace(re, block), "utf-8");
|
|
263
|
+
console.log(` ♻️ ${target} (replaced ${id})`);
|
|
264
|
+
} else {
|
|
265
|
+
const nl = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
266
|
+
writeFileSync(dest, existing + nl + block + "\n", "utf-8");
|
|
267
|
+
console.log(` ➕ ${target} (appended ${id})`);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case "merge-json": {
|
|
273
|
+
const existing = existsSync(dest) ? JSON.parse(readFileSync(dest, "utf-8")) : {};
|
|
274
|
+
const incoming = JSON.parse(content);
|
|
275
|
+
const merged = mergeJsonPreserve(existing, incoming);
|
|
276
|
+
writeFileSync(dest, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
277
|
+
console.log(` 🔀 ${target} (merged json)`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
default: {
|
|
282
|
+
const _exhaustive: never = strategy;
|
|
283
|
+
throw new Error(`Unknown file strategy: ${_exhaustive}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function blockDelim(ext: string): { start: string; end: string } {
|
|
289
|
+
if (ext === ".html" || ext === ".svelte") return { start: "<!--", end: "-->" };
|
|
290
|
+
if (ext === ".css") return { start: "/*", end: "*/" };
|
|
291
|
+
return { start: "//", end: "" };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function escapeRegex(s: string): string {
|
|
295
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Deep-merge `source` into `target`, preserving existing target values.
|
|
299
|
+
// Objects: recurse. Arrays: concat-dedupe by JSON identity. Primitives: keep target.
|
|
300
|
+
function mergeJsonPreserve(target: unknown, source: unknown): unknown {
|
|
301
|
+
if (Array.isArray(target) && Array.isArray(source)) {
|
|
302
|
+
const out = [...target];
|
|
303
|
+
for (const item of source) {
|
|
304
|
+
if (!out.some((x) => JSON.stringify(x) === JSON.stringify(item))) {
|
|
305
|
+
out.push(item);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
if (isPlainObject(target) && isPlainObject(source)) {
|
|
311
|
+
const out: Record<string, unknown> = { ...target };
|
|
312
|
+
for (const [k, v] of Object.entries(source)) {
|
|
313
|
+
out[k] = k in target ? mergeJsonPreserve(target[k], v) : v;
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
return target !== undefined ? target : source;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
321
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
163
322
|
}
|
|
164
323
|
|
|
165
324
|
// Re-exports for create.ts
|
package/src/cli/index.ts
CHANGED
|
@@ -10,43 +10,48 @@
|
|
|
10
10
|
const [, , command, ...args] = process.argv;
|
|
11
11
|
|
|
12
12
|
async function main() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
13
|
+
switch (command) {
|
|
14
|
+
case "create": {
|
|
15
|
+
const { runCreate } = await import("./create.ts");
|
|
16
|
+
await runCreate(args[0], args.slice(1));
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case "dev": {
|
|
20
|
+
const { runDev } = await import("./dev.ts");
|
|
21
|
+
await runDev();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "build": {
|
|
25
|
+
const { runBuild } = await import("./build.ts");
|
|
26
|
+
await runBuild();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "start": {
|
|
30
|
+
const { runStart } = await import("./start.ts");
|
|
31
|
+
await runStart();
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "test": {
|
|
35
|
+
const { runTest } = await import("./test.ts");
|
|
36
|
+
await runTest(args);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case "add": {
|
|
40
|
+
const { runAdd } = await import("./add.ts");
|
|
41
|
+
const addName = args.find((a) => !a.startsWith("--"));
|
|
42
|
+
const addFlags = args.filter((a) => a.startsWith("--"));
|
|
43
|
+
await runAdd(addName, addFlags);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "feat": {
|
|
47
|
+
const { runFeat } = await import("./feat.ts");
|
|
48
|
+
const featName = args.find((a) => !a.startsWith("--"));
|
|
49
|
+
const featFlags = args.filter((a) => a.startsWith("--"));
|
|
50
|
+
await runFeat(featName, featFlags);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
default: {
|
|
54
|
+
console.log(`
|
|
50
55
|
⬡ Bosia
|
|
51
56
|
|
|
52
57
|
Usage:
|
|
@@ -57,6 +62,7 @@ Commands:
|
|
|
57
62
|
dev Start the development server
|
|
58
63
|
build Build for production
|
|
59
64
|
start Run the production server
|
|
65
|
+
test [args] Run tests with bun test (auto-loads .env.test, sets BOSIA_ENV=test)
|
|
60
66
|
add <component> Add a UI component from the registry
|
|
61
67
|
feat <feature> Add a feature scaffold from the registry [--local]
|
|
62
68
|
|
|
@@ -66,16 +72,19 @@ Examples:
|
|
|
66
72
|
bosia dev
|
|
67
73
|
bosia build
|
|
68
74
|
bosia start
|
|
75
|
+
bosia test
|
|
76
|
+
bosia test --watch
|
|
77
|
+
bosia test --coverage
|
|
69
78
|
bosia add button → src/lib/components/ui/button/
|
|
70
79
|
bosia add shop/cart → src/lib/components/shop/cart/
|
|
71
80
|
bosia feat login
|
|
72
81
|
`);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
main().catch((err) => {
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
console.error("❌", err.message);
|
|
89
|
+
process.exit(1);
|
|
81
90
|
});
|