bosia 0.2.3 → 0.3.0

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