codz-cli 0.0.0-stage → 0.1.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 (117) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +523 -2
  4. package/assets/gradle-wrapper/README.txt +2 -0
  5. package/assets/gradle-wrapper/gradle-wrapper.jar +0 -0
  6. package/assets/gradle-wrapper/gradlew +251 -0
  7. package/assets/gradle-wrapper/gradlew.bat +94 -0
  8. package/bin/codz.js +33 -0
  9. package/dist/adapters/android.js +321 -0
  10. package/dist/adapters/androidSetup.js +277 -0
  11. package/dist/adapters/common.js +114 -0
  12. package/dist/adapters/dartGen.js +83 -0
  13. package/dist/adapters/flutter.js +291 -0
  14. package/dist/adapters/flutterGen.js +204 -0
  15. package/dist/adapters/flutterScan.js +312 -0
  16. package/dist/adapters/flutterSetup.js +252 -0
  17. package/dist/adapters/flutterStyles.js +130 -0
  18. package/dist/adapters/generic.js +20 -0
  19. package/dist/adapters/getx.js +423 -0
  20. package/dist/adapters/index.js +28 -0
  21. package/dist/adapters/ios.js +440 -0
  22. package/dist/adapters/kotlinGen.js +110 -0
  23. package/dist/adapters/models.js +116 -0
  24. package/dist/adapters/nextjs.js +243 -0
  25. package/dist/adapters/nextjsGen.js +285 -0
  26. package/dist/adapters/nextjsScan.js +364 -0
  27. package/dist/adapters/nextjsSetup.js +119 -0
  28. package/dist/adapters/react.js +89 -0
  29. package/dist/adapters/reactNative.js +109 -0
  30. package/dist/adapters/reactNativeSetup.js +236 -0
  31. package/dist/adapters/reactQuery.js +89 -0
  32. package/dist/adapters/reactSetup.js +150 -0
  33. package/dist/adapters/source.js +165 -0
  34. package/dist/cli.js +1175 -0
  35. package/dist/core/cache.js +76 -0
  36. package/dist/core/changeset.js +404 -0
  37. package/dist/core/commands.js +239 -0
  38. package/dist/core/config.js +45 -0
  39. package/dist/core/diff.js +143 -0
  40. package/dist/core/events.js +41 -0
  41. package/dist/core/fsutil.js +122 -0
  42. package/dist/core/git.js +36 -0
  43. package/dist/core/jsonEdit.js +113 -0
  44. package/dist/core/merge.js +159 -0
  45. package/dist/core/proc.js +23 -0
  46. package/dist/core/safe.js +75 -0
  47. package/dist/core/templates.js +100 -0
  48. package/dist/core/tsAst.js +112 -0
  49. package/dist/core/types.js +12 -0
  50. package/dist/core/validateConfig.js +172 -0
  51. package/dist/core/workflow.js +72 -0
  52. package/dist/core/workspace.js +80 -0
  53. package/dist/core/yaml.js +126 -0
  54. package/dist/core/yamlDoc.js +367 -0
  55. package/dist/detector.js +95 -0
  56. package/dist/input/apiSpec.js +395 -0
  57. package/dist/input/curl.js +259 -0
  58. package/dist/input/endpoint.js +220 -0
  59. package/dist/input/har.js +120 -0
  60. package/dist/input/infer.js +174 -0
  61. package/dist/input/naming.js +145 -0
  62. package/dist/input/paste.js +130 -0
  63. package/dist/skills/androidSkeleton.js +292 -0
  64. package/dist/skills/androidTests.js +221 -0
  65. package/dist/skills/androidUi.js +561 -0
  66. package/dist/skills/androidViews.js +536 -0
  67. package/dist/skills/apiAdd.js +99 -0
  68. package/dist/skills/apiDiff.js +271 -0
  69. package/dist/skills/apiEdit.js +547 -0
  70. package/dist/skills/apiImport.js +127 -0
  71. package/dist/skills/apiList.js +35 -0
  72. package/dist/skills/apiSync.js +221 -0
  73. package/dist/skills/auth.js +1304 -0
  74. package/dist/skills/completion.js +142 -0
  75. package/dist/skills/config.js +93 -0
  76. package/dist/skills/create.js +358 -0
  77. package/dist/skills/doctor.js +304 -0
  78. package/dist/skills/doctorFix.js +214 -0
  79. package/dist/skills/explain.js +61 -0
  80. package/dist/skills/feature.js +418 -0
  81. package/dist/skills/featureEdit.js +225 -0
  82. package/dist/skills/featureLinks.js +179 -0
  83. package/dist/skills/init.js +66 -0
  84. package/dist/skills/iosUi.js +468 -0
  85. package/dist/skills/locale.js +335 -0
  86. package/dist/skills/mcp.js +175 -0
  87. package/dist/skills/mock.js +482 -0
  88. package/dist/skills/module.js +23 -0
  89. package/dist/skills/plans.js +66 -0
  90. package/dist/skills/prefill.js +84 -0
  91. package/dist/skills/reactNativeUi.js +388 -0
  92. package/dist/skills/reactUi.js +352 -0
  93. package/dist/skills/report.js +75 -0
  94. package/dist/skills/route.js +230 -0
  95. package/dist/skills/screen.js +597 -0
  96. package/dist/skills/screenForm.js +410 -0
  97. package/dist/skills/setup.js +28 -0
  98. package/dist/skills/setupDoctor.js +226 -0
  99. package/dist/skills/templatesCmd.js +32 -0
  100. package/dist/skills/testgen.js +331 -0
  101. package/dist/skills/theme.js +388 -0
  102. package/dist/starters/auth.js +144 -0
  103. package/dist/starters/commands.js +283 -0
  104. package/dist/starters/engine.js +315 -0
  105. package/dist/starters/home.js +42 -0
  106. package/dist/starters/http.js +93 -0
  107. package/dist/starters/manifest.js +285 -0
  108. package/dist/starters/paths.js +70 -0
  109. package/dist/starters/publish.js +150 -0
  110. package/dist/starters/registry.js +258 -0
  111. package/dist/starters/sign.js +72 -0
  112. package/dist/starters/source.js +98 -0
  113. package/dist/starters/strictJson.js +148 -0
  114. package/dist/starters/tar.js +180 -0
  115. package/docs/COMMANDS.md +527 -0
  116. package/examples/openapi.yaml +319 -0
  117. package/package.json +57 -4
@@ -0,0 +1,243 @@
1
+ // Next.js adapter. Supported setup: App Router, TypeScript, fetch (directly
2
+ // or through a shared helper such as apiFetch) or axios (an axios.create
3
+ // instance, or axios itself), optionally with TanStack Query hooks. The Pages
4
+ // Router is detected and reported as not supported yet.
5
+ import { join } from 'node:path';
6
+ import { detectEol, exists, readText, walk, withEol } from "../core/fsutil.js";
7
+ import { ToolError } from "../core/types.js";
8
+ import { fileName, plural, singular } from "../input/naming.js";
9
+ import { referencedModels, resolveModels } from "./models.js";
10
+ import { nextArchitectures, planNextSetup } from "./nextjsSetup.js";
11
+ import { renderQueryHook, splitTsParams } from "./reactQuery.js";
12
+ import { applyTemplate } from "../core/templates.js";
13
+ import { addDefaultImport, addImports, appendBlock, chooseBase, findTsBlock, importPath, renderFunction, renderInterface, resourceOf, styled, tsType } from "./nextjsGen.js";
14
+ import { baseNoExt, dirOf, scan } from "./nextjsScan.js";
15
+ // Other modules import these from here.
16
+ export { findTsBlock, importPath } from "./nextjsGen.js";
17
+ export { stripJsonc } from "./nextjsScan.js";
18
+ export const VERSION = '0.1.0';
19
+ export const NEXT_CONVENTION_DOCS = {
20
+ templatesFolder: 'folder with custom templates (codz templates list)',
21
+ appDir: 'App Router folder',
22
+ apiFolder: 'where API functions live',
23
+ apiFileNaming: 'file naming in the API folder: kebab, camel, pascal or snake',
24
+ apiFileSuffix: 'word after the resource in API file names (for example "service"), or empty',
25
+ apiFileResource: 'resource form in API file names: plural (users.ts) or singular (user.ts)',
26
+ httpHelper: 'shared request helper to call, or "fetch" to call fetch directly',
27
+ httpHelperFile: 'file that exports the request helper',
28
+ baseUrlEnv: 'environment variable that holds the API base URL',
29
+ modelsFolder: 'where TypeScript interfaces for API data live',
30
+ modelsLayout: 'per-file (one interface per file) or single (all in modelsFile)',
31
+ modelsFile: 'the single models file, when modelsLayout is single',
32
+ modelFileNaming: 'file naming in the models folder',
33
+ importAlias: 'path alias prefix from tsconfig, for example @/',
34
+ aliasBase: 'folder the alias points to ("." for the project root)',
35
+ importStyle: 'how API files import models: alias or relative',
36
+ quote: 'string quote: single or double',
37
+ semicolons: 'true or false',
38
+ indent: 'indentation: 2, 4 or tab',
39
+ http: 'HTTP client: fetch or axios',
40
+ axiosInstance: 'axios instance API functions call (axios: the name exported from axiosInstanceFile, or "axios")',
41
+ axiosInstanceFile: 'file that creates the axios instance with axios.create (axios only)',
42
+ dataFetching: 'react-query to also write a TanStack Query hook for each endpoint, or none',
43
+ hooksFolder: 'where the query hooks go (react-query only)',
44
+ i18n: 'internationalization library, if any',
45
+ styling: 'styling library, if any',
46
+ };
47
+ async function planEndpoint(ctx, input, overlay, opts = {}) {
48
+ const { root, map } = ctx;
49
+ const c = map.conventions;
50
+ const files = new Map(overlay ?? []);
51
+ const read = (p) => files.get(p) ?? readText(join(root, p));
52
+ const summary = [];
53
+ const warnings = [...input.warnings];
54
+ const { spec, reused, replaced, notes } = resolveModels(input, map.models, (n) => n, { replace: opts.replaceModels });
55
+ warnings.push(...notes);
56
+ // Same endpoint already present?
57
+ const sameName = map.endpoints.find((e) => e.name === spec.name);
58
+ if (sameName) {
59
+ const base0 = chooseBase(spec, map, root);
60
+ if (sameName.method === spec.method && sameName.path.replace(/\{\w+\}/g, '{}') === base0.path.replace(/\{\w+\}/g, '{}')) {
61
+ return { files, summary: [`${spec.name} already exists in ${sameName.file}; nothing to do.`], warnings: [], generated: [] };
62
+ }
63
+ throw new ToolError('name_conflict', `A function called ${spec.name} already exists in ${sameName.file} for ${sameName.method} ${sameName.path}.`, 'Pass --name <functionName> to use another name.');
64
+ }
65
+ const quote = c.quote === 'double' ? '"' : "'";
66
+ const indent = c.indent === 'tab' ? '\t' : ' '.repeat(Number(c.indent) || 2);
67
+ const g = { c, style: { quote, semi: c.semicolons !== 'false', indent }, q: quote };
68
+ // Where each model lives (new or reused).
69
+ const modelFile = new Map();
70
+ for (const [name, info] of reused)
71
+ modelFile.set(name, info.file);
72
+ for (const m of spec.models) {
73
+ modelFile.set(m.name, replaced.get(m.name)?.file ?? (c.modelsLayout === 'single' && c.modelsFile ? c.modelsFile : `${c.modelsFolder}/${fileName(m.name, c.modelFileNaming)}.ts`));
74
+ }
75
+ // Write models.
76
+ const newModelFiles = [];
77
+ for (const m of spec.models) {
78
+ const file = modelFile.get(m.name);
79
+ const existing = read(file);
80
+ let content = existing ?? '';
81
+ const block = existing ? findTsBlock(existing, m.name) : null;
82
+ if (block && !replaced.has(m.name))
83
+ continue;
84
+ const deps = new Set();
85
+ for (const f of m.fields)
86
+ referencedModels(f.type, deps);
87
+ const byFile = new Map();
88
+ for (const d of deps) {
89
+ const df = modelFile.get(d);
90
+ if (!df || df === file)
91
+ continue;
92
+ byFile.set(df, [...(byFile.get(df) ?? []), d]);
93
+ }
94
+ const rendered = applyTemplate(root, 'model.ts', styled(renderInterface(m, g), g), { name: m.name }, warnings);
95
+ content = block ? content.slice(0, block.start) + rendered + content.slice(block.end) : appendBlock(content, rendered);
96
+ for (const [df, names] of [...byFile.entries()].sort())
97
+ content = addImports(content, importPath(file, df, { ...c, importStyle: c.modelsLayout === 'single' ? c.importStyle : 'relative' }), names, true, g);
98
+ if (existing === null)
99
+ newModelFiles.push(file);
100
+ files.set(file, existing === null ? content : withEol(content, detectEol(existing)));
101
+ summary.push(`${existing === null ? 'Create' : 'Update'} ${file}: interface ${m.name}${block ? ' (rewritten)' : ''}`);
102
+ }
103
+ // Models barrel file.
104
+ const modelsIndex = `${c.modelsFolder}/index.ts`;
105
+ const modelsIndexText = read(modelsIndex);
106
+ if (modelsIndexText && /export \* from/.test(modelsIndexText)) {
107
+ let t = modelsIndexText;
108
+ for (const f of newModelFiles) {
109
+ const spec2 = `./${baseNoExt(f)}`;
110
+ if (f === modelsIndex || t.includes(`'${spec2}'`) || t.includes(`"${spec2}"`))
111
+ continue;
112
+ t = t.replace(/\s*$/, '\n') + `export * from ${quote}${spec2}${quote}${g.style.semi ? ';' : ''}\n`;
113
+ }
114
+ if (t !== modelsIndexText) {
115
+ files.set(modelsIndex, withEol(t, detectEol(modelsIndexText)));
116
+ summary.push(`Update ${modelsIndex}: export the new models`);
117
+ }
118
+ }
119
+ // API function file.
120
+ const resource = spec.resource;
121
+ const existingForResource = map.endpoints.find((e) => resourceOf(e.path) === resourceOf(spec.path) && dirOf(e.file) === c.apiFolder);
122
+ let apiFile;
123
+ if (opts.targetFile) {
124
+ apiFile = opts.targetFile;
125
+ }
126
+ else if (existingForResource) {
127
+ apiFile = existingForResource.file;
128
+ }
129
+ else {
130
+ const word = c.apiFileResource === 'singular' ? singular(resource) : plural(singular(resource));
131
+ apiFile = `${c.apiFolder}/${fileName(c.apiFileSuffix ? `${word} ${c.apiFileSuffix}` : word, c.apiFileNaming)}.ts`;
132
+ }
133
+ const apiExisting = read(apiFile);
134
+ if (apiExisting && new RegExp(`(function|const)\\s+${spec.name}\\b`).test(apiExisting)) {
135
+ throw new ToolError('name_conflict', `${apiFile} already defines ${spec.name}.`, 'Pass --name <functionName> to use another name.');
136
+ }
137
+ const base = chooseBase(spec, map, root);
138
+ warnings.push(...base.warnings);
139
+ const fnText = styled(renderFunction(spec, base, g), g);
140
+ const fnParams = new RegExp(`function\\s+${spec.name}\\s*\\(([^)]*)\\)`).exec(fnText)?.[1] ?? '';
141
+ const fnReturn = fnText.match(/\):\s*Promise<(.+)>\s*\{/)?.[1] ?? '';
142
+ let apiText = appendBlock(apiExisting ?? '', applyTemplate(root, 'api-function.ts', fnText, { name: spec.name, method: spec.method, path: base.path, params: fnParams, returnType: fnReturn }, warnings));
143
+ const typeNames = new Set();
144
+ referencedModels(spec.response, typeNames);
145
+ referencedModels(spec.request, typeNames);
146
+ const typesByFile = new Map();
147
+ for (const t of typeNames) {
148
+ const f = modelFile.get(t);
149
+ typesByFile.set(f, [...(typesByFile.get(f) ?? []), t]);
150
+ }
151
+ for (const [f, names] of [...typesByFile.entries()].sort())
152
+ apiText = addImports(apiText, importPath(apiFile, f, c), names, true, g);
153
+ if (c.http === 'axios') {
154
+ const inst = c.axiosInstance || 'axios';
155
+ if (inst === 'axios' || !c.axiosInstanceFile)
156
+ apiText = addDefaultImport(apiText, 'axios', 'axios', g);
157
+ else {
158
+ let p = importPath(apiFile, c.axiosInstanceFile, { ...c, importStyle: 'relative' });
159
+ if (dirOf(c.axiosInstanceFile) !== dirOf(apiFile))
160
+ p = importPath(apiFile, c.axiosInstanceFile, c);
161
+ apiText = inst.startsWith('default:') ? addDefaultImport(apiText, p, inst.slice(8), g) : addImports(apiText, p, [inst], false, g);
162
+ }
163
+ }
164
+ else if (base.kind === 'helper') {
165
+ let helperPath = importPath(apiFile, c.httpHelperFile, { ...c, importStyle: 'relative' });
166
+ if (dirOf(c.httpHelperFile) !== dirOf(apiFile))
167
+ helperPath = importPath(apiFile, c.httpHelperFile, c);
168
+ apiText = addImports(apiText, helperPath, [c.httpHelper], false, g);
169
+ }
170
+ files.set(apiFile, apiExisting === null ? apiText : withEol(apiText, detectEol(apiExisting)));
171
+ summary.push(`${apiExisting === null ? 'Create' : 'Update'} ${apiFile}: ${spec.name}() for ${spec.method} ${spec.path}`);
172
+ // API barrel file.
173
+ if (apiExisting === null) {
174
+ const apiIndex = `${c.apiFolder}/index.ts`;
175
+ const indexText = read(apiIndex);
176
+ if (indexText !== null && /export \* from/.test(indexText)) {
177
+ const spec2 = `./${baseNoExt(apiFile)}`;
178
+ if (!indexText.includes(`'${spec2}'`) && !indexText.includes(`"${spec2}"`)) {
179
+ const t = indexText.replace(/\s*$/, '\n') + `export * from ${quote}${spec2}${quote}${g.style.semi ? ';' : ''}\n`;
180
+ files.set(apiIndex, withEol(t, detectEol(indexText)));
181
+ summary.push(`Update ${apiIndex}: export ${spec2}`);
182
+ }
183
+ }
184
+ else if (indexText !== null) {
185
+ warnings.push(`${apiIndex} does not use "export * from", so the new file was not added to it.`);
186
+ }
187
+ }
188
+ // TanStack Query hook.
189
+ if (c.dataFetching === 'react-query') {
190
+ const sig = new RegExp(`export\\s+(?:async\\s+)?function\\s+${spec.name}\\s*\\(([^)]*)\\)`).exec(apiText);
191
+ const hooksFolder = c.hooksFolder || (c.appDir?.startsWith('src/') ? 'src/hooks' : 'hooks');
192
+ const fromHook = (hf) => [importPath(hf, apiFile, c), importPath(hf, apiFile, { ...c, importStyle: 'relative' })];
193
+ const hookFiles = [...new Set([...walk(root, hooksFolder, { exts: ['.ts', '.tsx'] }), ...[...files.keys()].filter((f) => dirOf(f) === hooksFolder)])].sort();
194
+ let hookFile = hookFiles.find((hf) => fromHook(hf).some((ip) => new RegExp(`from\\s*['"]${ip.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}['"]`).test(read(hf) ?? '')));
195
+ if (!hookFile) {
196
+ const camelNames = hookFiles.some((hf) => /^use[A-Z]/.test(baseNoExt(hf)));
197
+ const word = c.apiFileResource === 'singular' ? singular(resourceOf(base.path)) : plural(singular(resourceOf(base.path)));
198
+ hookFile = `${hooksFolder}/${fileName(`use ${word}`, camelNames ? 'camel' : 'kebab')}.ts`;
199
+ }
200
+ const hookExisting = read(hookFile);
201
+ const hook = renderQueryHook(spec, spec.name, splitTsParams(sig?.[1] ?? ''), resourceOf(base.path), { q: quote, semi: g.style.semi ? ';' : '', indent });
202
+ if (sig && !new RegExp(`\\b(?:function|const)\\s+${hook.name}\\b`).test(hookExisting ?? '')) {
203
+ let ht = appendBlock(hookExisting ?? '', hook.code);
204
+ ht = addImports(ht, '@tanstack/react-query', hook.uses, false, g);
205
+ ht = addImports(ht, importPath(hookFile, apiFile, c), [spec.name], false, g);
206
+ const used = [...typesByFile.entries()].map(([f, names]) => [f, names.filter((n) => new RegExp(`\\b${n}\\b`).test(sig[1]))]).filter(([, n]) => n.length);
207
+ for (const [f, names] of used)
208
+ ht = addImports(ht, importPath(hookFile, f, c), [...names], true, g);
209
+ files.set(hookFile, hookExisting === null ? ht : withEol(ht, detectEol(hookExisting)));
210
+ summary.push(`${hookExisting === null ? 'Create' : 'Update'} ${hookFile}: ${hook.name}() (TanStack Query ${spec.method === 'GET' ? 'query' : 'mutation'})`);
211
+ }
212
+ }
213
+ return {
214
+ files,
215
+ summary,
216
+ warnings,
217
+ generated: [{ skill: 'api add', file: apiFile, symbol: spec.name, inputHash: '' }],
218
+ endpoints: [{ name: spec.name, method: spec.method, path: base.path, response: spec.response ? tsType(spec.response) : undefined, file: apiFile, calledFrom: [] }],
219
+ models: spec.models.map((m) => ({ name: m.name, file: modelFile.get(m.name), fields: m.fields.map((f) => ({ name: f.jsonName, type: tsType(f.type), nullable: f.nullable })) })),
220
+ };
221
+ }
222
+ export const nextjsAdapter = {
223
+ id: 'nextjs',
224
+ version: VERSION,
225
+ capabilities: { explain: true, apiAdd: true },
226
+ supportedSetup: 'Next.js App Router, TypeScript, fetch (directly or through a shared helper) or axios, optionally with TanStack Query hooks. Not yet: Pages Router, SWR.',
227
+ scan,
228
+ planEndpoint,
229
+ architectures: nextArchitectures,
230
+ planSetup: planNextSetup,
231
+ formatCommand(root, files) {
232
+ const bin = join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'prettier.cmd' : 'prettier');
233
+ if (!exists(bin))
234
+ return null;
235
+ return { cmd: bin, args: ['--write', ...files] };
236
+ },
237
+ verifyCommand(root) {
238
+ const bin = join(root, 'node_modules', '.bin', process.platform === 'win32' ? 'tsc.cmd' : 'tsc');
239
+ if (!exists(bin) || !exists(join(root, 'tsconfig.json')))
240
+ return null;
241
+ return { cmd: bin, args: ['--noEmit', '-p', 'tsconfig.json', '--pretty', 'false'] };
242
+ },
243
+ };
@@ -0,0 +1,285 @@
1
+ // Next.js adapter, part 2: writing code (types, interfaces, imports, the API
2
+ // function for each HTTP client style, and where its base URL comes from).
3
+ import { commentText, jsString, jsTemplateText } from "../core/safe.js";
4
+ import { join, posix } from 'node:path';
5
+ import { readText } from "../core/fsutil.js";
6
+ import {} from "../core/types.js";
7
+ import { specRelativePath } from "./common.js";
8
+ import { matchBrace } from "./source.js";
9
+ import { dirOf } from "./nextjsScan.js";
10
+ export function tsType(t) {
11
+ if (!t)
12
+ return 'unknown';
13
+ switch (t.kind) {
14
+ case 'string':
15
+ case 'date':
16
+ return 'string';
17
+ case 'int':
18
+ case 'double':
19
+ return 'number';
20
+ case 'bool':
21
+ return 'boolean';
22
+ case 'unknown':
23
+ return 'unknown';
24
+ case 'object':
25
+ return t.model;
26
+ case 'array': {
27
+ const inner = tsType(t.item);
28
+ return /[\s|]/.test(inner) ? `(${inner})[]` : `${inner}[]`;
29
+ }
30
+ }
31
+ }
32
+ export const IDENT = /^[A-Za-z_$][\w$]*$/;
33
+ export function propKey(key, q) {
34
+ return IDENT.test(key) ? key : jsString(key, q);
35
+ }
36
+ /** Applies the project's semicolon and indent style to generated text (written with 2 spaces and semicolons). */
37
+ export function styled(code, g) {
38
+ return code
39
+ .split('\n')
40
+ .map((line) => {
41
+ let l = line;
42
+ if (!g.style.semi)
43
+ l = l.replace(/;(\s*)$/, '$1');
44
+ const m = l.match(/^( +)/);
45
+ if (m && g.style.indent !== ' ')
46
+ l = g.style.indent.repeat(m[1].length / 2) + l.slice(m[1].length);
47
+ return l;
48
+ })
49
+ .join('\n');
50
+ }
51
+ export function renderInterface(m, g) {
52
+ const lines = [`export interface ${m.name} {`];
53
+ for (const f of m.fields) {
54
+ if (f.type.kind === 'date')
55
+ lines.push(` /** ISO 8601 date string. */`);
56
+ if (f.type.kind === 'unknown' || (f.type.kind === 'array' && f.type.item.kind === 'unknown'))
57
+ lines.push(` // codz: type unknown from the samples; set it by hand.`);
58
+ const t = tsType(f.type);
59
+ lines.push(` ${propKey(f.jsonName, g.q)}${f.nullable ? '?' : ''}: ${f.nullable && t !== 'unknown' ? `${t} | null` : t};`);
60
+ }
61
+ lines.push('}');
62
+ return lines.join('\n');
63
+ }
64
+ export function importPath(fromFile, toFile, c) {
65
+ const toNoExt = toFile.replace(/\.(tsx?|jsx?|mjs)$/, '').replace(/\/index$/, '');
66
+ const base = c.aliasBase === '.' ? '' : c.aliasBase + '/';
67
+ if (c.importStyle === 'alias' && c.importAlias && (base === '' || toNoExt.startsWith(base))) {
68
+ return c.importAlias + toNoExt.slice(base.length);
69
+ }
70
+ let r = posix.relative(dirOf(fromFile), toNoExt);
71
+ if (!r.startsWith('.'))
72
+ r = `./${r}`;
73
+ return r;
74
+ }
75
+ /** Adds named imports to a file, merging into an existing import from the same path. */
76
+ export function addImports(text, path, names, typeOnly, g) {
77
+ if (!names.length)
78
+ return text;
79
+ const q = g.q;
80
+ const esc = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
81
+ const existing = new RegExp(`import\\s+${typeOnly ? 'type\\s+' : ''}\\{([^}]*)\\}\\s*from\\s*['"]${esc}['"];?`).exec(text);
82
+ if (existing) {
83
+ const have = existing[1].split(',').map((s) => s.trim()).filter(Boolean);
84
+ const merged = [...new Set([...have, ...names])].sort();
85
+ if (merged.length === have.length)
86
+ return text;
87
+ const line = `import ${typeOnly ? 'type ' : ''}{ ${merged.join(', ')} } from ${q}${path}${q}${g.style.semi ? ';' : ''}`;
88
+ return text.slice(0, existing.index) + line + text.slice(existing.index + existing[0].length);
89
+ }
90
+ const line = `import ${typeOnly ? 'type ' : ''}{ ${[...names].sort().join(', ')} } from ${q}${path}${q}${g.style.semi ? ';' : ''}`;
91
+ const imports = [...text.matchAll(/^import[\s\S]*?from\s*['"][^'"]+['"];?[ \t]*$/gm)];
92
+ if (imports.length) {
93
+ const last = imports[imports.length - 1];
94
+ const at = last.index + last[0].length;
95
+ return `${text.slice(0, at)}\n${line}${text.slice(at)}`;
96
+ }
97
+ return text.length ? `${line}\n\n${text}` : `${line}\n`;
98
+ }
99
+ /** Adds `import name from 'path'` unless the file already imports that default. */
100
+ export function addDefaultImport(text, path, name, g) {
101
+ const esc = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
102
+ if (new RegExp(`import\\s+${name}\\b[^;\\n]*from\\s*['"]${esc}['"]`).test(text))
103
+ return text;
104
+ const line = `import ${name} from ${g.q}${path}${g.q}${g.style.semi ? ';' : ''}`;
105
+ const imports = [...text.matchAll(/^import[\s\S]*?from\s*['"][^'"]+['"];?[ \t]*$/gm)];
106
+ if (imports.length)
107
+ return `${line}\n${text}`;
108
+ return text.length ? `${line}\n\n${text}` : `${line}\n`;
109
+ }
110
+ /** Finds `export interface Name {...}` or `export type Name = {...};` (with a doc comment above it). */
111
+ export function findTsBlock(text, name) {
112
+ const m = new RegExp(`(^|\\n)((?:[ \\t]*(?:/\\*\\*(?:(?!\\*/)[^])*\\*/|//[^\\n]*)\\n)*)[ \\t]*export\\s+(?:interface\\s+${name}\\b[^{]*|type\\s+${name}\\b[^=]*=\\s*)\\{`).exec(text);
113
+ if (!m)
114
+ return null;
115
+ const open = m.index + m[0].length - 1;
116
+ const close = matchBrace(text, open);
117
+ if (close === -1)
118
+ return null;
119
+ let end = close + 1;
120
+ if (text[end] === ';')
121
+ end++;
122
+ return { start: m.index + m[1].length, end };
123
+ }
124
+ export function appendBlock(text, block) {
125
+ if (!text.trim())
126
+ return block + '\n';
127
+ return text.replace(/\s*$/, '') + '\n\n' + block + '\n';
128
+ }
129
+ export function chooseBase(input, map, root = '.') {
130
+ const c = map.conventions;
131
+ const warnings = [];
132
+ // An endpoint from an API spec goes relative to the project's base URL when there is one: the spec's
133
+ // server (https://api.example.com/v1) is a stand-in for it, usually another environment.
134
+ const usesHelper = (c.http === 'axios' && !!c.axiosInstanceFile && /\bbaseURL\s*:/.test(readText(join(root, c.axiosInstanceFile)) ?? '')) || (c.http !== 'axios' && !!c.httpHelper && c.httpHelper !== 'fetch');
135
+ const projectBase = map.baseUrls.find((b) => b.name === c.baseUrlEnv)?.value ?? map.baseUrls[0]?.value;
136
+ const fromSpec = specRelativePath(input, projectBase);
137
+ const spec = fromSpec !== null && (usesHelper || projectBase || c.baseUrlEnv) ? { ...input, origin: '', path: fromSpec } : input;
138
+ const prefixOf = (value) => {
139
+ try {
140
+ return new URL(value).pathname.replace(/\/$/, '');
141
+ }
142
+ catch {
143
+ return '';
144
+ }
145
+ };
146
+ const originOf = (value) => {
147
+ try {
148
+ return new URL(value).origin;
149
+ }
150
+ catch {
151
+ return '';
152
+ }
153
+ };
154
+ const candidates = map.baseUrls.filter((b) => !spec.origin || originOf(b.value) === spec.origin);
155
+ const matched = candidates.find((b) => b.name === c.baseUrlEnv && spec.path.startsWith(prefixOf(b.value))) ?? candidates.find((b) => spec.path.startsWith(prefixOf(b.value)));
156
+ const path = matched ? spec.path.slice(prefixOf(matched.value).length) || '/' : spec.path;
157
+ if (spec.origin && !matched) {
158
+ const known = map.baseUrls.map((b) => `${b.name}=${b.value}`).join(', ');
159
+ if (known)
160
+ warnings.push(`The URL host ${spec.origin} does not match any configured base URL (${known}).`);
161
+ }
162
+ if (c.http === 'axios' && c.axiosInstanceFile && /\bbaseURL\s*:/.test(readText(join(root, c.axiosInstanceFile)) ?? '')) {
163
+ if (spec.origin && !matched && warnings.length)
164
+ warnings[warnings.length - 1] += ` The call goes through the ${c.axiosInstance.replace(/^default:/, '')} axios instance, which uses its own baseURL; check that this endpoint lives on the same server.`;
165
+ return { kind: 'helper', path, warnings };
166
+ }
167
+ if (c.http !== 'axios' && c.httpHelper && c.httpHelper !== 'fetch') {
168
+ if (spec.origin && !matched)
169
+ warnings[warnings.length - 1] += ` The call goes through ${c.httpHelper}, which uses its own base URL; check that this endpoint lives on the same server.`;
170
+ return { kind: 'helper', path, warnings };
171
+ }
172
+ if (matched && /^\w+$/.test(matched.name))
173
+ return { kind: 'env', env: matched.name, path, warnings };
174
+ if (c.baseUrlEnv && /^\w+$/.test(c.baseUrlEnv))
175
+ return { kind: 'env', env: c.baseUrlEnv, path, warnings };
176
+ if (spec.origin) {
177
+ warnings.push(`No base URL setting was found, so the host ${spec.origin} is written into the code. Consider moving it to an environment variable.`);
178
+ return { kind: 'literal', origin: spec.origin, path, warnings };
179
+ }
180
+ return { kind: 'literal', origin: '', path, warnings };
181
+ }
182
+ export function renderFunction(spec, base, g) {
183
+ const q = g.q;
184
+ const params = spec.pathParams.map((p) => `${p.name}: ${p.type === 'int' ? 'number' : 'string'}`);
185
+ if (spec.request)
186
+ params.push(`body: ${tsType(spec.request)}`);
187
+ if (spec.queryParams.length) {
188
+ params.push(`params?: { ${spec.queryParams.map((p) => `${propKey(p.name, q)}?: ${tsType(p.type)}`).join('; ')} }`);
189
+ }
190
+ const hasResponse = !!spec.response && spec.method !== 'DELETE' && spec.method !== 'HEAD';
191
+ const resultType = hasResponse ? tsType(spec.response) : spec.method === 'DELETE' || spec.method === 'HEAD' ? 'void' : 'unknown';
192
+ // Literal text is escaped for a template literal; only {param} placeholders become ${param}.
193
+ const pathExpr = base.path.split(/(\{\w+\})/).map((part) => (/^\{\w+\}$/.test(part) ? `\${${part.slice(1, -1)}}` : jsTemplateText(part))).join('');
194
+ const lines = [];
195
+ lines.push(`/** ${spec.method} ${commentText(spec.path)} */`);
196
+ const needsQuery = spec.queryParams.length > 0;
197
+ const urlTail = needsQuery ? '${queryString ? `?${queryString}` : \'\'}'.replace(/'/g, q) : '';
198
+ const queryLines = needsQuery
199
+ ? [
200
+ ' const searchParams = new URLSearchParams();',
201
+ ...spec.queryParams.map((p) => {
202
+ const check = IDENT.test(p.name) ? `params?.${p.name}` : `params?.[${jsString(p.name, q)}]`;
203
+ const access = IDENT.test(p.name) ? `params.${p.name}` : `params[${jsString(p.name, q)}]`;
204
+ return ` if (${check} !== undefined) searchParams.set(${jsString(p.name, q)}, String(${access}));`;
205
+ }),
206
+ ' const queryString = searchParams.toString();',
207
+ ]
208
+ : [];
209
+ if (g.c.http === 'axios') {
210
+ // axios serializes params (skipping undefined) and parses JSON itself.
211
+ const client = (g.c.axiosInstance || 'axios').replace(/^default:/, '');
212
+ const prefix = base.kind === 'env' ? `\${process.env.${base.env}}` : base.kind === 'literal' ? jsTemplateText(base.origin ?? '') : '';
213
+ const genericType = spec.unwrapData ? `{ data: ${resultType} }` : resultType;
214
+ const verb = spec.method.toLowerCase();
215
+ const config = needsQuery ? '{ params }' : '';
216
+ const withBody = ['post', 'put', 'patch'].includes(verb);
217
+ const args = [`\`${prefix}${pathExpr}\``];
218
+ if (withBody)
219
+ args.push(spec.request ? 'body' : config ? 'undefined' : '');
220
+ if (config)
221
+ args.push(config);
222
+ const callArgs = args.filter((a, i) => a !== '' || i === 0).join(', ');
223
+ lines.push(`export async function ${spec.name}(${params.join(', ')}): Promise<${resultType}> {`);
224
+ if (resultType === 'void')
225
+ lines.push(` await ${client}.${verb}(${callArgs});`);
226
+ else {
227
+ lines.push(` const res = await ${client}.${verb}<${genericType}>(${callArgs});`);
228
+ lines.push(spec.unwrapData ? ' return res.data.data;' : ' return res.data;');
229
+ }
230
+ lines.push('}');
231
+ return lines.join('\n');
232
+ }
233
+ const init = [];
234
+ if (spec.method !== 'GET')
235
+ init.push(`method: ${q}${spec.method}${q}`);
236
+ if (spec.request)
237
+ init.push('body: JSON.stringify(body)');
238
+ if (base.kind === 'helper') {
239
+ const helper = g.c.httpHelper;
240
+ const genericType = spec.unwrapData ? `{ data: ${resultType} }` : resultType;
241
+ const call = `${helper}<${genericType}>(\`${pathExpr}${urlTail}\`${init.length ? `, { ${init.join(', ')} }` : ''})`;
242
+ if (!needsQuery && !spec.unwrapData) {
243
+ lines.push(`export function ${spec.name}(${params.join(', ')}): Promise<${resultType}> {`);
244
+ lines.push(` return ${call};`);
245
+ }
246
+ else {
247
+ lines.push(`export async function ${spec.name}(${params.join(', ')}): Promise<${resultType}> {`);
248
+ lines.push(...queryLines);
249
+ if (spec.unwrapData) {
250
+ lines.push(` const res = await ${call};`);
251
+ lines.push(' return res.data;');
252
+ }
253
+ else {
254
+ lines.push(` return ${call};`);
255
+ }
256
+ }
257
+ lines.push('}');
258
+ return lines.join('\n');
259
+ }
260
+ const prefix = base.kind === 'env' ? `\${process.env.${base.env}}` : jsTemplateText(base.origin ?? '');
261
+ const headers = spec.request ? [`headers: { ${q}Content-Type${q}: ${q}application/json${q} }`] : [];
262
+ const fetchInit = [`method: ${q}${spec.method}${q}`, ...headers, ...(spec.request ? ['body: JSON.stringify(body)'] : [])];
263
+ lines.push(`export async function ${spec.name}(${params.join(', ')}): Promise<${resultType}> {`);
264
+ lines.push(...queryLines);
265
+ lines.push(` const res = await fetch(\`${prefix}${pathExpr}${urlTail}\`, {`);
266
+ for (const f of fetchInit)
267
+ lines.push(` ${f},`);
268
+ lines.push(' });');
269
+ lines.push(` if (!res.ok) throw new Error(\`${spec.name} failed: \${res.status}\`);`);
270
+ if (resultType === 'void') {
271
+ // Nothing to parse.
272
+ }
273
+ else if (spec.unwrapData) {
274
+ lines.push(` const json = (await res.json()) as { data: ${resultType} };`);
275
+ lines.push(' return json.data;');
276
+ }
277
+ else {
278
+ lines.push(` return (await res.json()) as ${resultType};`);
279
+ }
280
+ lines.push('}');
281
+ return lines.join('\n');
282
+ }
283
+ export const resourceOf = (path) => path
284
+ .split('/')
285
+ .filter((s) => s && !s.startsWith('{') && !/^(api|v\d+(\.\d+)?|rest)$/i.test(s))[0] ?? 'root';