@fumadocs/cli 1.3.2 → 1.3.4

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 (47) hide show
  1. package/dist/build/index.d.ts +4 -6
  2. package/dist/build/index.d.ts.map +1 -1
  3. package/dist/build/index.js +10 -18
  4. package/dist/build/index.js.map +1 -1
  5. package/dist/{client-YTcWP1iz.js → client-C2A4Jf2w.js} +1 -1
  6. package/dist/{client-YTcWP1iz.js.map → client-C2A4Jf2w.js.map} +1 -1
  7. package/dist/{config-Dyass4D9.d.ts → config-BIpj98KJ.d.ts} +9 -5
  8. package/dist/config-BIpj98KJ.d.ts.map +1 -0
  9. package/dist/config.d.ts +2 -2
  10. package/dist/config.js +77 -1
  11. package/dist/config.js.map +1 -0
  12. package/dist/{fs-CigSthjp.js → fs-C3j4H046.js} +1 -1
  13. package/dist/{fs-CigSthjp.js.map → fs-C3j4H046.js.map} +1 -1
  14. package/dist/{ast-BRNdmLn5.js → import-CSbSteB3.js} +56 -2
  15. package/dist/import-CSbSteB3.js.map +1 -0
  16. package/dist/index.js +13 -15
  17. package/dist/index.js.map +1 -1
  18. package/dist/installer-BWoLnsXE.js +673 -0
  19. package/dist/installer-BWoLnsXE.js.map +1 -0
  20. package/dist/registry/client.d.ts +17 -9
  21. package/dist/registry/client.d.ts.map +1 -1
  22. package/dist/registry/client.js +1 -1
  23. package/dist/registry/installer/index.d.ts +17 -27
  24. package/dist/registry/installer/index.d.ts.map +1 -1
  25. package/dist/registry/installer/index.js +1 -3
  26. package/dist/registry/macros/route-handler.d.ts +21 -0
  27. package/dist/registry/macros/route-handler.d.ts.map +1 -0
  28. package/dist/registry/macros/route-handler.js +11 -0
  29. package/dist/registry/macros/route-handler.js.map +1 -0
  30. package/dist/registry/schema.d.ts +2 -2
  31. package/dist/registry/schema.js +14 -11
  32. package/dist/registry/schema.js.map +1 -1
  33. package/dist/{schema-DrgqlhpT.d.ts → schema-BAaUX4uu.d.ts} +21 -10
  34. package/dist/schema-BAaUX4uu.d.ts.map +1 -0
  35. package/dist/schema.json +1 -0
  36. package/dist/types-79PW0lgM.d.ts +6 -0
  37. package/dist/types-79PW0lgM.d.ts.map +1 -0
  38. package/package.json +6 -6
  39. package/dist/ast-BRNdmLn5.js.map +0 -1
  40. package/dist/config-BYrMmXOw.js +0 -55
  41. package/dist/config-BYrMmXOw.js.map +0 -1
  42. package/dist/config-Dyass4D9.d.ts.map +0 -1
  43. package/dist/installer-B3-my9zN.js +0 -248
  44. package/dist/installer-B3-my9zN.js.map +0 -1
  45. package/dist/schema/default.json +0 -1
  46. package/dist/schema/src.json +0 -1
  47. package/dist/schema-DrgqlhpT.d.ts.map +0 -1
@@ -0,0 +1,673 @@
1
+ import { a as transformSpecifiers, i as toImportSpecifier, n as encodeImport, o as typescriptExtensions, r as collectMacroBindings, t as decodeImport } from "./import-CSbSteB3.js";
2
+ import { r as createCache, t as HttpRegistryClient } from "./client-C2A4Jf2w.js";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { x } from "tinyexec";
6
+ import { Visitor, parse } from "oxc-parser";
7
+ import { detect } from "package-manager-detector";
8
+ import MagicString from "magic-string";
9
+ //#region src/registry/installer/dep-manager.ts
10
+ async function createDeps(cwd, dependencies, devDependencies) {
11
+ const packageJsonPath = path.join(cwd, "package.json");
12
+ return new DependencyManager(cwd, await fs.readFile(packageJsonPath).then((res) => JSON.parse(res.toString())).catch(() => null), dependencies, devDependencies);
13
+ }
14
+ var DependencyManager = class {
15
+ constructor(cwd, packageJson, dependencies, devDependencies) {
16
+ this.cwd = cwd;
17
+ this.packageJson = packageJson;
18
+ const installedDeps = {
19
+ ...packageJson?.dependencies,
20
+ ...packageJson?.devDependencies
21
+ };
22
+ this.dependencies = Object.entries(dependencies).filter(([k]) => !(k in installedDeps)).map(([k, v]) => encodeDep(k, v));
23
+ this.devDependencies = Object.entries(devDependencies).filter(([k]) => !(k in installedDeps)).map(([k, v]) => encodeDep(k, v));
24
+ }
25
+ hasRequired() {
26
+ return this.dependencies.length > 0 || this.devDependencies.length > 0;
27
+ }
28
+ async writeRequired(packageJsonPath = path.resolve(this.cwd, "package.json")) {
29
+ if (this.packageJson === null) return false;
30
+ for (const dep of this.dependencies) {
31
+ const { name, version } = decodeDep(dep);
32
+ this.packageJson.dependencies ??= {};
33
+ this.packageJson.dependencies[name] ??= version;
34
+ }
35
+ for (const dep of this.devDependencies) {
36
+ const { name, version } = decodeDep(dep);
37
+ this.packageJson.devDependencies ??= {};
38
+ this.packageJson.devDependencies[name] ??= version;
39
+ }
40
+ await fs.writeFile(packageJsonPath, JSON.stringify(this.packageJson, null, 2));
41
+ }
42
+ async installRequired(packageManager) {
43
+ packageManager ??= (await detect())?.name ?? "npm";
44
+ if (this.dependencies.length > 0) await x(packageManager, ["install", ...this.dependencies]);
45
+ if (this.devDependencies.length > 0) await x(packageManager, [
46
+ "install",
47
+ ...this.devDependencies,
48
+ "-D"
49
+ ]);
50
+ }
51
+ };
52
+ function encodeDep(name, version) {
53
+ return version === null || version.length === 0 ? name : `${name}@${version}`;
54
+ }
55
+ function decodeDep(dep) {
56
+ const idx = dep.indexOf("@", 1);
57
+ if (idx === -1) return {
58
+ name: dep,
59
+ version: "latest"
60
+ };
61
+ else return {
62
+ name: dep.slice(0, idx),
63
+ version: dep.slice(idx + 1)
64
+ };
65
+ }
66
+ //#endregion
67
+ //#region src/utils/format.ts
68
+ function indent(code, tab = 1) {
69
+ const prefix = " ".repeat(tab);
70
+ return code.split("\n").map((v) => v.length === 0 ? v : prefix + v).join("\n");
71
+ }
72
+ function dedent(code) {
73
+ const lines = code.split("\n");
74
+ const minIndent = lines.reduce((min, line) => {
75
+ const match = line.match(/^(\s*)\S/);
76
+ return match ? Math.min(min, match[1].length) : min;
77
+ }, Infinity);
78
+ return minIndent === Infinity ? lines.join("\n") : lines.map((l) => l.slice(minIndent)).join("\n");
79
+ }
80
+ //#endregion
81
+ //#region src/registry/macros/route-handler.build.ts
82
+ const reactRouterLoaderMethods = new Set([
83
+ "GET",
84
+ "HEAD",
85
+ "OPTIONS"
86
+ ]);
87
+ const reactRouterActionMethods = new Set([
88
+ "POST",
89
+ "PUT",
90
+ "PATCH",
91
+ "DELETE"
92
+ ]);
93
+ function collectRouteHandlerCalls(program, locals) {
94
+ const calls = [];
95
+ new Visitor({ CallExpression(node) {
96
+ if (node.callee.type !== "Identifier") return;
97
+ if (!locals.has(node.callee.name)) return;
98
+ calls.push(node);
99
+ } }).visit(program);
100
+ return calls;
101
+ }
102
+ function isSameCall(init, call) {
103
+ if (!init || init.type !== "CallExpression") return false;
104
+ return init.start === call.start && init.end === call.end;
105
+ }
106
+ function findStatementSpanForCall(program, call) {
107
+ for (const stmt of program.body) {
108
+ const span = statementSpanIfContainsCall(stmt, call);
109
+ if (span) return span;
110
+ }
111
+ return null;
112
+ }
113
+ function statementSpanIfContainsCall(stmt, call) {
114
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "VariableDeclaration") {
115
+ for (const d of stmt.declaration.declarations) if (isSameCall(d.init, call)) return {
116
+ start: stmt.start,
117
+ end: stmt.end
118
+ };
119
+ }
120
+ if (stmt.type === "VariableDeclaration") {
121
+ for (const d of stmt.declarations) if (isSameCall(d.init, call)) return {
122
+ start: stmt.start,
123
+ end: stmt.end
124
+ };
125
+ }
126
+ return null;
127
+ }
128
+ function objectPropertyKeyName(key) {
129
+ if (key.type !== "Property") return null;
130
+ const k = key.key;
131
+ if (k.type === "Identifier") return k.name;
132
+ if (k.type === "Literal" && typeof k.value === "string") return k.value;
133
+ return null;
134
+ }
135
+ /**
136
+ * Read `methods`, `params`, and optional `catchAll` from the `$routeHandler` info object literal (AST).
137
+ */
138
+ function parseRouteInfoFromAst(info) {
139
+ let methods;
140
+ let params;
141
+ let catchAll;
142
+ for (const prop of info.properties) {
143
+ if (prop.type !== "Property") continue;
144
+ const name = objectPropertyKeyName(prop);
145
+ if (name === "methods") {
146
+ if (prop.value.type !== "ArrayExpression") throw new Error("route-handler.build: `methods` in $routeHandler info must be an array literal");
147
+ const out = [];
148
+ for (const el of prop.value.elements) {
149
+ if (el == null) continue;
150
+ if (el.type === "Literal" && typeof el.value === "string") {
151
+ out.push(el.value);
152
+ continue;
153
+ }
154
+ throw new Error("route-handler.build: `methods` must be string literals (e.g. methods: [\"GET\", \"POST\"])");
155
+ }
156
+ methods = out;
157
+ }
158
+ if (name === "params") {
159
+ if (prop.value.type !== "ArrayExpression") throw new Error("route-handler.build: `params` must be an array literal");
160
+ const out = [];
161
+ for (const el of prop.value.elements) {
162
+ if (el == null) continue;
163
+ if (el.type === "Literal" && typeof el.value === "string") {
164
+ out.push(el.value);
165
+ continue;
166
+ }
167
+ throw new Error("route-handler.build: `params` must be string literals");
168
+ }
169
+ params = out;
170
+ }
171
+ if (name === "catchAll") {
172
+ if (prop.value.type === "Literal" && typeof prop.value.value === "string") {
173
+ catchAll = prop.value.value;
174
+ continue;
175
+ }
176
+ throw new Error("route-handler.build: `catchAll` must be a string literal");
177
+ }
178
+ }
179
+ if (!methods?.length) throw new Error("route-handler.build: $routeHandler info must include a non-empty `methods` array");
180
+ if (!params) throw new Error("route-handler.build: $routeHandler info must include a `params` array literal");
181
+ return {
182
+ methods,
183
+ params,
184
+ catchAll
185
+ };
186
+ }
187
+ function needsRouteParams(info) {
188
+ return info.params.length > 0 || Boolean(info.catchAll);
189
+ }
190
+ function encodeKey(key) {
191
+ if (key.includes("-")) return JSON.stringify(key);
192
+ return key;
193
+ }
194
+ function bindingNameFromParam(p) {
195
+ if (p.type === "Identifier") return p.name;
196
+ return null;
197
+ }
198
+ function parseHandlerFromAst(s, expr) {
199
+ const isFn = expr.type === "FunctionExpression";
200
+ const isArrow = expr.type === "ArrowFunctionExpression";
201
+ if (!isFn && !isArrow) throw new Error("route-handler.build: second argument to $routeHandler must be a function or async arrow function");
202
+ const fn = expr;
203
+ if (!fn.async) throw new Error("route-handler.build: route handler must be async");
204
+ if (isFn && (!fn.body || fn.body.type !== "BlockStatement")) throw new Error("route-handler.build: function handler must use a block body");
205
+ const p0 = fn.params[0];
206
+ const p1 = fn.params[1];
207
+ if (fn.params.length > 2) throw new Error("route-handler.build: route handler must have at most two parameters");
208
+ const requestName = p0 && bindingNameFromParam(p0) || "request";
209
+ const paramsName = p1 ? bindingNameFromParam(p1) : null;
210
+ if (p1 && !paramsName) throw new Error("route-handler.build: unsupported parameter pattern; use a simple identifier for (request, params)");
211
+ const body = fn.body;
212
+ let bodyText;
213
+ if (body == null) throw new Error("route-handler.build: handler has no body");
214
+ else if (body.type === "BlockStatement") bodyText = s.original.slice(body.start + 1, body.end - 1).replace(/^\s*?\n/, "").replace(/\n\s*?$/, "");
215
+ else if (fn.type === "ArrowFunctionExpression") bodyText = `return ${s.original.slice(body.start, body.end)};`;
216
+ else throw new Error("route-handler.build: could not extract handler body");
217
+ return {
218
+ requestName,
219
+ paramsName,
220
+ bodyText
221
+ };
222
+ }
223
+ function generateRequestDeclaration(framework, binding) {
224
+ if (framework === "tanstack-start") {
225
+ if (binding === "ctx") throw new Error("route-handler.build: name the request parameter something other than `ctx` for TanStack file routes");
226
+ return `const ${binding} = ctx.request;\n`;
227
+ }
228
+ if (framework === "react-router") {
229
+ if (binding === "args") throw new Error("route-handler.build: name the request parameter something other than `args` for React Router resource routes");
230
+ return `const ${binding} = args.request;\n`;
231
+ }
232
+ return "";
233
+ }
234
+ function generateParamsDeclaration(framework, info, paramsBinding) {
235
+ let paramsIdentifier;
236
+ let paramsCatchAllIdentifier;
237
+ switch (framework) {
238
+ case "next": return `const ${paramsBinding} = await ctx.params;\n`;
239
+ case "waku": return `const ${paramsBinding} = context.params;\n`;
240
+ case "react-router":
241
+ paramsIdentifier = "args.params";
242
+ paramsCatchAllIdentifier = "args.params['*']";
243
+ break;
244
+ case "tanstack-start":
245
+ paramsIdentifier = "ctx.params";
246
+ paramsCatchAllIdentifier = "ctx.params._splat";
247
+ break;
248
+ }
249
+ const parts = [];
250
+ for (const k of info.params) parts.push(`${encodeKey(k)}: ${paramsIdentifier}.${k}`);
251
+ if (info.catchAll) parts.push(`${encodeKey(info.catchAll)}: ${paramsCatchAllIdentifier}`);
252
+ return `const ${paramsBinding} = {\n${indent(parts.join(",\n"))}\n};\n`;
253
+ }
254
+ function resolveParamsBindingName(info, userSecond) {
255
+ if (!needsRouteParams(info)) return null;
256
+ return userSecond ?? "params";
257
+ }
258
+ function registryRouteToTanStackCreateFileRoutePath(route) {
259
+ const segments = route.split("/");
260
+ const parts = [];
261
+ for (const seg of segments) {
262
+ if (/^\[\[\.\.\.[^/\]]+\]\]$/.test(seg) || /^\[\.\.\.[^/\]]+\]$/.test(seg)) {
263
+ parts.push("$");
264
+ continue;
265
+ }
266
+ const m = /^\[([^/\]]+)\]$/.exec(seg);
267
+ if (m) parts.push(`$${m[1]}`);
268
+ else parts.push(seg);
269
+ }
270
+ return `/${parts.join("/")}`;
271
+ }
272
+ /** URL path for `RouteContext` / `ApiContext` string literals (leading slash, App Router–style segments). */
273
+ function registryRouteToUrlPath(route) {
274
+ const t = route.replace(/^\/+/, "").replace(/\/+$/, "");
275
+ return t ? `/${t}` : "/";
276
+ }
277
+ /**
278
+ * React Router typegen: `./+types/<segments>` relative to the route file (see `+types` next to `routes/`).
279
+ */
280
+ function computeReactRouterTypesSpecifier(routeFilePath) {
281
+ return `./+types/${path.basename(routeFilePath, path.extname(routeFilePath))}`;
282
+ }
283
+ function generateImports(framework, routeFilePath) {
284
+ const lines = [];
285
+ if (framework === "tanstack-start") lines.push(`import { createFileRoute } from '@tanstack/react-router';`);
286
+ if (framework === "waku") lines.push(`import type { ApiContext } from 'waku/router';`);
287
+ if (framework === "react-router") lines.push(`import type { Route } from '${computeReactRouterTypesSpecifier(routeFilePath)}';`);
288
+ return lines.length ? `${lines.join("\n")}\n` : "";
289
+ }
290
+ function generateDeclaration(framework, route, parsedInfo, handler) {
291
+ const paramsBinding = resolveParamsBindingName(parsedInfo, handler.paramsName);
292
+ let inner = dedent(handler.bodyText);
293
+ if (paramsBinding) inner = generateParamsDeclaration(framework, parsedInfo, paramsBinding) + inner;
294
+ inner = generateRequestDeclaration(framework, handler.requestName) + inner;
295
+ const urlPath = registryRouteToUrlPath(route);
296
+ const urlPathLiteral = JSON.stringify(urlPath);
297
+ switch (framework) {
298
+ case "next": {
299
+ /** `RouteContext` is provided by Next.js typed routes / `next typegen` (global). */
300
+ const ctxType = `RouteContext<${urlPathLiteral}>`;
301
+ return parsedInfo.methods.map((m) => `export async function ${m}(${handler.requestName}: Request, ctx: ${ctxType}) {\n${indent(inner)}\n}`).join("\n\n");
302
+ }
303
+ case "waku": {
304
+ const ctxType = `ApiContext<${urlPathLiteral}>`;
305
+ return parsedInfo.methods.map((m) => `export async function ${m}(${handler.requestName}: Request, context: ${ctxType}) {\n${indent(inner)}\n}`).join("\n\n");
306
+ }
307
+ case "tanstack-start": return `export const Route = createFileRoute(${JSON.stringify(registryRouteToTanStackCreateFileRoutePath(route))})({\n server: {\n handlers: {\n${parsedInfo.methods.map((m) => ` ${m}: async (ctx) => {\n${indent(inner, 4)}\n },`).join("\n")}\n },\n },\n});\n`;
308
+ case "react-router": {
309
+ const includeLoader = parsedInfo.methods.some((x) => reactRouterLoaderMethods.has(x));
310
+ const includeAction = parsedInfo.methods.some((x) => reactRouterActionMethods.has(x));
311
+ if (!includeLoader && !includeAction) throw new Error("route-handler.build: react-router needs at least one method mapped to loader (GET/HEAD/OPTIONS) or action (POST/PUT/PATCH/DELETE)");
312
+ const parts = [];
313
+ if (includeLoader) parts.push(`export async function loader(args: Route.LoaderArgs) {\n${indent(inner)}\n}`);
314
+ if (includeAction) parts.push(`export async function action(args: Route.ActionArgs) {\n${indent(inner)}\n}`);
315
+ return `${parts.join("\n\n")}\n`;
316
+ }
317
+ }
318
+ }
319
+ function removeMacroImport(s, importDecl) {
320
+ const start = importDecl.start;
321
+ let end = importDecl.end;
322
+ if (s.original[end] === "\n") end += 1;
323
+ s.remove(start, end);
324
+ }
325
+ /**
326
+ * Rewrites a module that calls `$routeHandler` into framework-native route code.
327
+ *
328
+ * Output does not import `@fumadocs/cli`.
329
+ *
330
+ * Uses framework typegen where applicable: global `RouteContext` (Next typed routes), `ApiContext` (Waku),
331
+ * inferred handler `ctx` (TanStack `createFileRoute`), `Route.LoaderArgs` / `Route.ActionArgs` (React Router `+types`).
332
+ */
333
+ function transformRouteHandler(route, routeFilePath, framework, program, s) {
334
+ const macro = collectMacroBindings(program, "$routeHandler");
335
+ if (!macro) return;
336
+ const calls = collectRouteHandlerCalls(program, macro.locals);
337
+ if (calls.length === 0) return;
338
+ if (calls.length > 1) throw new Error("route-handler.build: expected exactly one $routeHandler(...) call per file");
339
+ const call = calls[0];
340
+ if (call.arguments.length !== 2) throw new Error("route-handler.build: $routeHandler must be called with (info, handler)");
341
+ const arg0 = call.arguments[0];
342
+ const arg1 = call.arguments[1];
343
+ if (arg0.type !== "ObjectExpression") throw new Error("route-handler.build: first argument to $routeHandler must be an object literal");
344
+ const parsedInfo = parseRouteInfoFromAst(arg0);
345
+ const handler = parseHandlerFromAst(s, arg1);
346
+ const stmtSpan = findStatementSpanForCall(program, call);
347
+ if (!stmtSpan) throw new Error("route-handler.build: $routeHandler(...) must be the initializer of a const (optionally exported)");
348
+ for (const decl of macro.importDecls) removeMacroImport(s, decl);
349
+ const extraImports = generateImports(framework, routeFilePath);
350
+ if (extraImports) {
351
+ const insertPos = findInsertPositionForNewImports(program);
352
+ if (insertPos === 0) s.prepend(extraImports);
353
+ else s.appendLeft(insertPos, `\n${extraImports}`);
354
+ }
355
+ s.overwrite(stmtSpan.start, stmtSpan.end, generateDeclaration(framework, route, parsedInfo, handler));
356
+ s.trim();
357
+ }
358
+ function findInsertPositionForNewImports(program) {
359
+ const last = program.body.findLast((stmt) => stmt.type === "ImportDeclaration");
360
+ if (last) return last.end;
361
+ return 0;
362
+ }
363
+ //#endregion
364
+ //#region src/utils/framework.ts
365
+ /**
366
+ * Resolves the route file path (relative to the CLI `baseDir`) from an App Router–style route string.
367
+ *
368
+ * @param route - The route path, e.g., "api/auth/[[...slug]]".
369
+ * @param framework - Value of `framework` in CLI config.
370
+ * @param extension - Optional file extension, default is "ts".
371
+ * @returns Relative path incl. framework route root, e.g. "app/api/foo/route.ts" (next), "routes/api.foo.$.ts" (tanstack-start), or "routes/api/foo/$.ts" (react-router).
372
+ */
373
+ function resolveRouteFilePath(route, framework, extension = "ts") {
374
+ route = route.replace(/^\/+/, "").replace(/\/+$/, "");
375
+ switch (framework) {
376
+ case "next": return `app/${route}/route.${extension}`;
377
+ case "tanstack-start": {
378
+ let flat = route.replace(/\[\[\.\.\.[^/\]]+\]\]/g, "$").replace(/\[\.\.\.[^/\]]+\]/g, "$").replace(/\[([^/\]]+)\]/g, (_, p1) => `$${p1}`);
379
+ flat = flat.replaceAll("/", ".");
380
+ return `routes/${flat}.${extension}`;
381
+ }
382
+ case "react-router": return `app/routes/${route.replace(/\[\[\.\.\.[^/\]]+\]\]/g, "all").replace(/\[\.\.\.[^/\]]+\]/g, "all").replace(/\[([^/\]]+)\]/g, (_, v) => `$${v}`)}.${extension}`;
383
+ case "waku": return `pages/_api/${route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "[...$1]").replace(/\[\.\.\.([^\]]+)\]/g, "[...$1]")}.${extension}`;
384
+ default: return framework;
385
+ }
386
+ }
387
+ function resolveReactRouterRoute(route) {
388
+ return route.replace(/\[\[\.\.\.[^/\]]+\]\]/g, "*").replace(/\[\.\.\.[^/\]]+\]/g, "*").replace(/\[([^/\]]+)\]/g, (_, v) => `:${v}`);
389
+ }
390
+ function tsSingleQuotedLiteral(value) {
391
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
392
+ }
393
+ function unwrapExportDefaultToArrayExpression(decl) {
394
+ if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") return null;
395
+ let expr = decl;
396
+ while (expr) {
397
+ if (expr.type === "ArrayExpression") return expr;
398
+ if (expr.type === "TSSatisfiesExpression" || expr.type === "TSAsExpression") {
399
+ expr = expr.expression;
400
+ continue;
401
+ }
402
+ return null;
403
+ }
404
+ return null;
405
+ }
406
+ function parseStringArgument(expr) {
407
+ if (!expr) return null;
408
+ if (expr.type === "Literal" && typeof expr.value === "string") return expr.value;
409
+ return null;
410
+ }
411
+ function parseReactRouterRoutesItem(item) {
412
+ if (!item || item.type !== "CallExpression") return;
413
+ const callee = item.callee;
414
+ if (callee.type !== "Identifier") return;
415
+ if (callee.name === "route") {
416
+ const path = parseStringArgument(item.arguments[0]);
417
+ const module = parseStringArgument(item.arguments[1]);
418
+ if (path === null || module === null) return;
419
+ return {
420
+ type: "route",
421
+ path,
422
+ module,
423
+ expression: item
424
+ };
425
+ }
426
+ if (callee.name === "index") {
427
+ const module = parseStringArgument(item.arguments[0]);
428
+ if (module === null) return;
429
+ return {
430
+ type: "index",
431
+ module,
432
+ expression: item
433
+ };
434
+ }
435
+ }
436
+ function findReactRouterRoutesArray(program) {
437
+ for (const stmt of program.body) {
438
+ if (stmt.type !== "ExportDefaultDeclaration") continue;
439
+ if (stmt.declaration == null) continue;
440
+ return unwrapExportDefaultToArrayExpression(stmt.declaration);
441
+ }
442
+ return null;
443
+ }
444
+ /**
445
+ * Inserts a `route(...)` or `index(...)` entry into a React Router `routes.ts` config
446
+ * (`export default [ ... ] satisfies RouteConfig`), writing the file in place.
447
+ *
448
+ * New `route` entries are placed before the first `route('*', ...)` splat (if any) so the
449
+ * catch-all remains last.
450
+ */
451
+ async function addReactRouterRouteToFile(routesFilePath, content, spec, write = true) {
452
+ const parsed = await parse(routesFilePath, content, {
453
+ lang: routesFilePath.endsWith(".tsx") ? "tsx" : "ts",
454
+ astType: "ts"
455
+ });
456
+ if (parsed.errors.length > 0) throw new Error(`addReactRouterRouteToFile: failed to parse ${routesFilePath}:\n${parsed.errors.map((e) => e.message).join("\n")}`);
457
+ const array = findReactRouterRoutesArray(parsed.program);
458
+ if (!array) throw new Error(`addReactRouterRouteToFile: no export default array found in ${routesFilePath}`);
459
+ const elements = [];
460
+ for (const v of array.elements) if (v && v.type !== "SpreadElement") {
461
+ const parsed = parseReactRouterRoutesItem(v);
462
+ if (parsed) elements.push(parsed);
463
+ }
464
+ if (spec.kind === "index") {
465
+ if (elements.some((element) => element.type === "index")) return {
466
+ added: false,
467
+ content
468
+ };
469
+ } else if (elements.some((element) => element.type === "route" && element.path === spec.path)) return {
470
+ added: false,
471
+ content
472
+ };
473
+ const line = spec.kind === "index" ? `index(${tsSingleQuotedLiteral(spec.module)})` : `route(${tsSingleQuotedLiteral(spec.path)}, ${tsSingleQuotedLiteral(spec.module)})`;
474
+ const s = new MagicString(content);
475
+ const insertAt = elements.findLast((element) => element.type !== "route" || !element.path.endsWith("*"));
476
+ if (!insertAt) s.appendRight(array.start + 1, `\n ${line},\n`);
477
+ else s.appendRight(insertAt.expression.end, `,\n ${line}`);
478
+ const out = s.toString();
479
+ if (write) await fs.writeFile(routesFilePath, out, "utf8");
480
+ return {
481
+ added: true,
482
+ content: out
483
+ };
484
+ }
485
+ //#endregion
486
+ //#region src/registry/installer/index.ts
487
+ var ComponentInstaller = class {
488
+ constructor(rootClient, options = {}) {
489
+ this.rootClient = rootClient;
490
+ this.installedFiles = /* @__PURE__ */ new Set();
491
+ this.downloadCache = createCache();
492
+ this.dependencies = {};
493
+ this.devDependencies = {};
494
+ this.cwd = options.cwd ?? process.cwd();
495
+ this.plugins = options.plugins ?? [];
496
+ }
497
+ async installComponent(comp, ctx) {
498
+ if (ctx.stack.indexOf(comp) !== ctx.stack.length - 1) return;
499
+ const pluginCtx = {
500
+ installer: this,
501
+ ...ctx
502
+ };
503
+ for (const plugin of this.plugins) comp = await plugin.beforeInstall?.(comp, pluginCtx) ?? comp;
504
+ Object.assign(this.dependencies, comp.dependencies);
505
+ Object.assign(this.devDependencies, comp.devDependencies);
506
+ for (const file of comp.files) {
507
+ const outPath = this.resolveOutputPath(file);
508
+ if (this.installedFiles.has(outPath)) continue;
509
+ this.installedFiles.add(outPath);
510
+ const output = typescriptExtensions.includes(path.extname(outPath)) ? await this.transform(file, comp, ctx) : file.content;
511
+ const status = await fs.readFile(outPath).then((res) => {
512
+ if (res.toString().trim() === output.trim()) return "ignore";
513
+ return "need-update";
514
+ }).catch(() => "write");
515
+ if (status === "ignore") continue;
516
+ if (status === "need-update") {
517
+ if (!await ctx.io.confirmFileOverride({ path: outPath })) continue;
518
+ }
519
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
520
+ await fs.writeFile(outPath, output);
521
+ ctx.io.onFileDownloaded({
522
+ path: outPath,
523
+ file,
524
+ component: comp
525
+ });
526
+ }
527
+ for (const child of comp.$subComponents) {
528
+ const stack = [...ctx.stack, child];
529
+ const variables = { ...ctx.$variables };
530
+ if (child.$registry.registryId !== comp.$registry.registryId) {
531
+ const info = await child.$registry.fetchRegistryInfo();
532
+ Object.assign(variables, info.variables);
533
+ }
534
+ Object.assign(variables, child.variables);
535
+ await this.installComponent(child, {
536
+ ...ctx,
537
+ stack,
538
+ $variables: variables
539
+ });
540
+ }
541
+ }
542
+ async install(name, io) {
543
+ let downloaded;
544
+ const registry = (await this.rootClient.fetchRegistryInfo()).registries?.find((registry) => name.startsWith(`${registry}/`));
545
+ if (registry) downloaded = await this.download(name.slice(registry.length + 1), this.rootClient.createLinkedRegistryClient(registry));
546
+ else downloaded = await this.download(name, this.rootClient);
547
+ const allComponents = /* @__PURE__ */ new Set();
548
+ function scan(comp) {
549
+ if (allComponents.has(comp)) return;
550
+ allComponents.add(comp);
551
+ for (const child of comp.$subComponents) scan(child);
552
+ }
553
+ scan(downloaded);
554
+ const importLookup = /* @__PURE__ */ new Map();
555
+ for (const comp of allComponents) for (const file of comp.files) importLookup.set(encodeImport(file), file);
556
+ const info = await downloaded.$registry.fetchRegistryInfo();
557
+ await this.installComponent(downloaded, {
558
+ importLookup,
559
+ io,
560
+ $variables: {
561
+ ...info.env,
562
+ ...downloaded.variables
563
+ },
564
+ stack: [downloaded]
565
+ });
566
+ }
567
+ deps() {
568
+ return createDeps(this.cwd, this.dependencies, this.devDependencies);
569
+ }
570
+ async onEnd() {
571
+ const config = this.rootClient.config;
572
+ if (config.commands.format) await x(config.commands.format);
573
+ }
574
+ /**
575
+ * download component & its sub components
576
+ */
577
+ async download(name, client) {
578
+ return this.downloadCache.cached(JSON.stringify([client.registryId, name]), async (presolve) => {
579
+ for (const plugin of this.plugins) await plugin.beforeDownload?.({
580
+ installer: this,
581
+ name
582
+ });
583
+ const comp = await client.fetchComponent(name);
584
+ const result = {
585
+ ...comp,
586
+ $registry: client,
587
+ $subComponents: []
588
+ };
589
+ presolve(result);
590
+ result.$subComponents = await Promise.all(comp.subComponents.map((sub) => {
591
+ if (typeof sub === "string") return this.download(sub, client);
592
+ let subClient;
593
+ if (this.rootClient instanceof HttpRegistryClient) {
594
+ const baseUrl = new URL(sub.baseUrl, `${this.rootClient.baseUrl}/`).href;
595
+ subClient = client instanceof HttpRegistryClient && client.baseUrl === baseUrl ? client : new HttpRegistryClient(baseUrl, client.config);
596
+ } else subClient = new HttpRegistryClient(sub.baseUrl, client.config);
597
+ return this.download(sub.component, subClient);
598
+ }));
599
+ for (const plugin of this.plugins) await plugin.afterDownload?.({
600
+ installer: this,
601
+ name,
602
+ result
603
+ });
604
+ return result;
605
+ });
606
+ }
607
+ async transform(file, component, ctx) {
608
+ const filePath = this.resolveOutputPath(file);
609
+ const transformCtx = {
610
+ installer: this,
611
+ file,
612
+ filePath,
613
+ component,
614
+ ...ctx
615
+ };
616
+ let transformed = await this.defaultTransform(file.content, transformCtx);
617
+ for (const plugin of this.plugins) {
618
+ if (!plugin.transform) continue;
619
+ transformed = await plugin.transform(transformed, transformCtx);
620
+ }
621
+ return transformed;
622
+ }
623
+ async defaultTransform(content, ctx) {
624
+ const { file, importLookup, filePath, $variables, io } = ctx;
625
+ const config = this.rootClient.config;
626
+ const parsed = await parse(filePath, content);
627
+ const s = new MagicString(content);
628
+ transformSpecifiers(parsed.program, s, (specifier) => {
629
+ for (const plugin of this.plugins) if (plugin.transformImport) specifier = plugin.transformImport(specifier, ctx);
630
+ if (importLookup.has(specifier)) {
631
+ let outputPath = this.resolveOutputPath(importLookup.get(specifier));
632
+ for (const [k, v] of Object.entries($variables)) if (typeof v === "string") outputPath = outputPath.replaceAll(`<${k}>`, v);
633
+ return toImportSpecifier(filePath, outputPath);
634
+ }
635
+ const decoded = decodeImport(specifier);
636
+ if ("raw" in decoded) return decoded.raw;
637
+ io.onWarn(`cannot find the referenced file of ${specifier}`);
638
+ return specifier;
639
+ });
640
+ if (file.type === "route-handler") {
641
+ transformRouteHandler(file.route, filePath, config.framework, parsed.program, s);
642
+ if (config.framework === "react-router") {
643
+ const routesFile = path.join(this.cwd, "app/routes.ts");
644
+ const content = await fs.readFile(routesFile, "utf-8").then((res) => res.toString()).catch(() => null);
645
+ if (content) await addReactRouterRouteToFile(routesFile, content, {
646
+ path: resolveReactRouterRoute(file.route),
647
+ module: path.relative(path.dirname(routesFile), filePath)
648
+ });
649
+ }
650
+ }
651
+ return s.toString();
652
+ }
653
+ resolveOutputPath(file) {
654
+ const config = this.rootClient.config;
655
+ if (file.type === "route-handler") {
656
+ const rel = resolveRouteFilePath(file.route, config.framework, "ts");
657
+ return path.resolve(this.cwd, config.baseDir, rel);
658
+ }
659
+ const dir = {
660
+ components: config.aliases.componentsDir,
661
+ ui: config.aliases.uiDir,
662
+ css: config.aliases.cssDir,
663
+ lib: config.aliases.libDir,
664
+ layout: config.aliases.layoutDir
665
+ }[file.type];
666
+ if (file.target) return path.resolve(this.cwd, config.baseDir, file.target.replace("<dir>", dir));
667
+ return path.resolve(this.cwd, config.baseDir, dir, path.basename(file.path));
668
+ }
669
+ };
670
+ //#endregion
671
+ export { ComponentInstaller as t };
672
+
673
+ //# sourceMappingURL=installer-BWoLnsXE.js.map