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.
- package/README.md +39 -39
- package/package.json +56 -54
- 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 +266 -258
- 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 -156
- package/src/core/client/appState.svelte.ts +33 -31
- package/src/core/client/enhance.ts +83 -78
- package/src/core/client/hydrate.ts +95 -81
- 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 -148
- 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 +184 -145
- 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 -122
- package/src/core/renderer.ts +359 -286
- 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 +538 -424
- package/src/core/types.ts +25 -20
- 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
|
@@ -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
|
-
|
|
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] ──────────────────────
|
|
@@ -17,29 +17,29 @@ import {
|
|
|
17
17
|
// Supports nested feature dependencies (e.g. todo → drizzle).
|
|
18
18
|
|
|
19
19
|
type FileStrategy =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
+
registryRoot = root;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export async function installFeature(name: string, isRoot: boolean, options?: InstallOptions) {
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
321
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
314
322
|
}
|
|
315
323
|
|
|
316
324
|
// Re-exports for create.ts
|