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.
- package/BETA.md +91 -0
- package/LICENSE +21 -0
- package/README.md +523 -2
- package/assets/gradle-wrapper/README.txt +2 -0
- package/assets/gradle-wrapper/gradle-wrapper.jar +0 -0
- package/assets/gradle-wrapper/gradlew +251 -0
- package/assets/gradle-wrapper/gradlew.bat +94 -0
- package/bin/codz.js +33 -0
- package/dist/adapters/android.js +321 -0
- package/dist/adapters/androidSetup.js +277 -0
- package/dist/adapters/common.js +114 -0
- package/dist/adapters/dartGen.js +83 -0
- package/dist/adapters/flutter.js +291 -0
- package/dist/adapters/flutterGen.js +204 -0
- package/dist/adapters/flutterScan.js +312 -0
- package/dist/adapters/flutterSetup.js +252 -0
- package/dist/adapters/flutterStyles.js +130 -0
- package/dist/adapters/generic.js +20 -0
- package/dist/adapters/getx.js +423 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/ios.js +440 -0
- package/dist/adapters/kotlinGen.js +110 -0
- package/dist/adapters/models.js +116 -0
- package/dist/adapters/nextjs.js +243 -0
- package/dist/adapters/nextjsGen.js +285 -0
- package/dist/adapters/nextjsScan.js +364 -0
- package/dist/adapters/nextjsSetup.js +119 -0
- package/dist/adapters/react.js +89 -0
- package/dist/adapters/reactNative.js +109 -0
- package/dist/adapters/reactNativeSetup.js +236 -0
- package/dist/adapters/reactQuery.js +89 -0
- package/dist/adapters/reactSetup.js +150 -0
- package/dist/adapters/source.js +165 -0
- package/dist/cli.js +1175 -0
- package/dist/core/cache.js +76 -0
- package/dist/core/changeset.js +404 -0
- package/dist/core/commands.js +239 -0
- package/dist/core/config.js +45 -0
- package/dist/core/diff.js +143 -0
- package/dist/core/events.js +41 -0
- package/dist/core/fsutil.js +122 -0
- package/dist/core/git.js +36 -0
- package/dist/core/jsonEdit.js +113 -0
- package/dist/core/merge.js +159 -0
- package/dist/core/proc.js +23 -0
- package/dist/core/safe.js +75 -0
- package/dist/core/templates.js +100 -0
- package/dist/core/tsAst.js +112 -0
- package/dist/core/types.js +12 -0
- package/dist/core/validateConfig.js +172 -0
- package/dist/core/workflow.js +72 -0
- package/dist/core/workspace.js +80 -0
- package/dist/core/yaml.js +126 -0
- package/dist/core/yamlDoc.js +367 -0
- package/dist/detector.js +95 -0
- package/dist/input/apiSpec.js +395 -0
- package/dist/input/curl.js +259 -0
- package/dist/input/endpoint.js +220 -0
- package/dist/input/har.js +120 -0
- package/dist/input/infer.js +174 -0
- package/dist/input/naming.js +145 -0
- package/dist/input/paste.js +130 -0
- package/dist/skills/androidSkeleton.js +292 -0
- package/dist/skills/androidTests.js +221 -0
- package/dist/skills/androidUi.js +561 -0
- package/dist/skills/androidViews.js +536 -0
- package/dist/skills/apiAdd.js +99 -0
- package/dist/skills/apiDiff.js +271 -0
- package/dist/skills/apiEdit.js +547 -0
- package/dist/skills/apiImport.js +127 -0
- package/dist/skills/apiList.js +35 -0
- package/dist/skills/apiSync.js +221 -0
- package/dist/skills/auth.js +1304 -0
- package/dist/skills/completion.js +142 -0
- package/dist/skills/config.js +93 -0
- package/dist/skills/create.js +358 -0
- package/dist/skills/doctor.js +304 -0
- package/dist/skills/doctorFix.js +214 -0
- package/dist/skills/explain.js +61 -0
- package/dist/skills/feature.js +418 -0
- package/dist/skills/featureEdit.js +225 -0
- package/dist/skills/featureLinks.js +179 -0
- package/dist/skills/init.js +66 -0
- package/dist/skills/iosUi.js +468 -0
- package/dist/skills/locale.js +335 -0
- package/dist/skills/mcp.js +175 -0
- package/dist/skills/mock.js +482 -0
- package/dist/skills/module.js +23 -0
- package/dist/skills/plans.js +66 -0
- package/dist/skills/prefill.js +84 -0
- package/dist/skills/reactNativeUi.js +388 -0
- package/dist/skills/reactUi.js +352 -0
- package/dist/skills/report.js +75 -0
- package/dist/skills/route.js +230 -0
- package/dist/skills/screen.js +597 -0
- package/dist/skills/screenForm.js +410 -0
- package/dist/skills/setup.js +28 -0
- package/dist/skills/setupDoctor.js +226 -0
- package/dist/skills/templatesCmd.js +32 -0
- package/dist/skills/testgen.js +331 -0
- package/dist/skills/theme.js +388 -0
- package/dist/starters/auth.js +144 -0
- package/dist/starters/commands.js +283 -0
- package/dist/starters/engine.js +315 -0
- package/dist/starters/home.js +42 -0
- package/dist/starters/http.js +93 -0
- package/dist/starters/manifest.js +285 -0
- package/dist/starters/paths.js +70 -0
- package/dist/starters/publish.js +150 -0
- package/dist/starters/registry.js +258 -0
- package/dist/starters/sign.js +72 -0
- package/dist/starters/source.js +98 -0
- package/dist/starters/strictJson.js +148 -0
- package/dist/starters/tar.js +180 -0
- package/docs/COMMANDS.md +527 -0
- package/examples/openapi.yaml +319 -0
- package/package.json +57 -4
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// GetX features in the GetX CLI layout:
|
|
2
|
+
//
|
|
3
|
+
// lib/app/modules/<name>/bindings/<name>_binding.dart
|
|
4
|
+
// lib/app/modules/<name>/controllers/<name>_controller.dart
|
|
5
|
+
// lib/app/modules/<name>/views/<name>_view.dart
|
|
6
|
+
// lib/app/routes/app_pages.dart (GetPage list)
|
|
7
|
+
// lib/app/routes/app_routes.dart (Routes and _Paths constants)
|
|
8
|
+
//
|
|
9
|
+
// `module add` creates a module and registers its route. `api wire` adds an
|
|
10
|
+
// endpoint call to a module's controller with loading, data and error state.
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { detectEol, readText, withEol } from "../core/fsutil.js";
|
|
13
|
+
import { ToolError } from "../core/types.js";
|
|
14
|
+
import { camel, DART_RESERVED, kebab, pascal, snake } from "../input/naming.js";
|
|
15
|
+
import { addDartImport, addToClass, importUri, insertGetxRegistration } from "./flutter.js";
|
|
16
|
+
import { matchBrace } from "./source.js";
|
|
17
|
+
export function getxPaths(c, moduleName) {
|
|
18
|
+
const s = snake(moduleName);
|
|
19
|
+
const folder = `${c.modulesFolder || 'lib/app/modules'}/${s}`;
|
|
20
|
+
const pages = c.routesFile || 'lib/app/routes/app_pages.dart';
|
|
21
|
+
return {
|
|
22
|
+
snake: s,
|
|
23
|
+
pascal: pascal(moduleName),
|
|
24
|
+
binding: `${folder}/bindings/${s}_binding.dart`,
|
|
25
|
+
controller: `${folder}/controllers/${s}_controller.dart`,
|
|
26
|
+
view: `${folder}/views/${s}_view.dart`,
|
|
27
|
+
pages,
|
|
28
|
+
routes: pages.replace(/[^/]+$/, 'app_routes.dart'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function requireGetx(c) {
|
|
32
|
+
if (c.di !== 'getx' && c.stateManagement !== 'getx') {
|
|
33
|
+
throw new ToolError('not_supported', 'Modules are generated for GetX projects so far; this project does not use GetX.', 'For a fresh project, run: codz setup --architecture getx');
|
|
34
|
+
}
|
|
35
|
+
if (!c.packageName)
|
|
36
|
+
throw new ToolError('no_package', 'pubspec.yaml has no name, so imports cannot be written.');
|
|
37
|
+
}
|
|
38
|
+
function checkModuleName(name) {
|
|
39
|
+
const s = snake(name);
|
|
40
|
+
if (!s || /^[0-9]/.test(s) || DART_RESERVED.has(s) || DART_RESERVED.has(camel(name))) {
|
|
41
|
+
throw new ToolError('bad_name', `"${name}" cannot be used as a module name.`, 'Use letters, for example: profile, order_details, settings');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Route constants follow the file: UPPER_CASE when it already uses that (GetX CLI default), else lowerCamel (lint-clean). */
|
|
45
|
+
function routeConst(name, upper) {
|
|
46
|
+
return upper ? snake(name).toUpperCase() : camel(name);
|
|
47
|
+
}
|
|
48
|
+
/** Inserts a line before the closing brace of a class, keeping one statement per line. */
|
|
49
|
+
function insertInClass(text, className, line) {
|
|
50
|
+
const m = new RegExp(`\\bclass\\s+${className}\\b[^{]*\\{`).exec(text);
|
|
51
|
+
if (!m)
|
|
52
|
+
throw new ToolError('class_not_found', `Could not find class ${className}.`);
|
|
53
|
+
const close = matchBrace(text, m.index + m[0].length - 1, 'dart');
|
|
54
|
+
const before = text.slice(0, close).replace(/\s*$/, '');
|
|
55
|
+
return `${before}\n ${line}\n${text.slice(close)}`;
|
|
56
|
+
}
|
|
57
|
+
function bindingFile(p, c) {
|
|
58
|
+
let t = [
|
|
59
|
+
`class ${p.pascal}Binding extends Bindings {`,
|
|
60
|
+
' @override',
|
|
61
|
+
' void dependencies() {',
|
|
62
|
+
` Get.lazyPut<${p.pascal}Controller>(() => ${p.pascal}Controller());`,
|
|
63
|
+
' }',
|
|
64
|
+
'}',
|
|
65
|
+
].join('\n') + '\n';
|
|
66
|
+
t = addDartImport(t, 'package:get/get.dart');
|
|
67
|
+
return addDartImport(t, importUri(p.binding, p.controller, c));
|
|
68
|
+
}
|
|
69
|
+
function controllerFile(p) {
|
|
70
|
+
return addDartImport(`class ${p.pascal}Controller extends GetxController {}\n`, 'package:get/get.dart');
|
|
71
|
+
}
|
|
72
|
+
function viewFile(p, c) {
|
|
73
|
+
const title = p.snake.split('_').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
74
|
+
let t = [
|
|
75
|
+
`class ${p.pascal}View extends GetView<${p.pascal}Controller> {`,
|
|
76
|
+
` const ${p.pascal}View({super.key});`,
|
|
77
|
+
'',
|
|
78
|
+
' @override',
|
|
79
|
+
' Widget build(BuildContext context) {',
|
|
80
|
+
' return Scaffold(',
|
|
81
|
+
` appBar: AppBar(title: const Text('${title.replace(/'/g, "\\'")}')),`,
|
|
82
|
+
` body: const Center(child: Text('${p.pascal}View is working')),`,
|
|
83
|
+
' );',
|
|
84
|
+
' }',
|
|
85
|
+
'}',
|
|
86
|
+
].join('\n') + '\n';
|
|
87
|
+
t = addDartImport(t, 'package:flutter/material.dart');
|
|
88
|
+
t = addDartImport(t, 'package:get/get.dart');
|
|
89
|
+
return addDartImport(t, importUri(p.view, p.controller, c));
|
|
90
|
+
}
|
|
91
|
+
function getPageEntry(p, constName, indent) {
|
|
92
|
+
return [`${indent}GetPage(`, `${indent} name: _Paths.${constName},`, `${indent} page: () => const ${p.pascal}View(),`, `${indent} binding: ${p.pascal}Binding(),`, `${indent}),`].join('\n');
|
|
93
|
+
}
|
|
94
|
+
export function planGetxModule(ctx, opts, overlay = new Map()) {
|
|
95
|
+
const { root, map } = ctx;
|
|
96
|
+
const c = map.conventions;
|
|
97
|
+
requireGetx(c);
|
|
98
|
+
checkModuleName(opts.name);
|
|
99
|
+
const files = new Map(overlay);
|
|
100
|
+
const read = (path) => files.get(path) ?? readText(join(root, path));
|
|
101
|
+
const summary = [];
|
|
102
|
+
const warnings = [];
|
|
103
|
+
const p = getxPaths(c, opts.name);
|
|
104
|
+
for (const f of [p.binding, p.controller, p.view]) {
|
|
105
|
+
if (read(f) !== null) {
|
|
106
|
+
throw new ToolError('module_exists', `The ${p.snake} module already exists (${f}).`, 'Pick another name, or wire an API into it with: codz api wire <endpoint> --into ' + p.snake);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
files.set(p.binding, bindingFile(p, c));
|
|
110
|
+
files.set(p.controller, controllerFile(p));
|
|
111
|
+
files.set(p.view, viewFile(p, c));
|
|
112
|
+
summary.push(`Create ${p.binding}: ${p.pascal}Binding`, `Create ${p.controller}: ${p.pascal}Controller`, `Create ${p.view}: ${p.pascal}View`);
|
|
113
|
+
// Routes.
|
|
114
|
+
const pagesText = read(p.pages);
|
|
115
|
+
const routesText = read(p.routes);
|
|
116
|
+
const routePath = `/${kebab(opts.name)}`;
|
|
117
|
+
if (pagesText === null || routesText === null) {
|
|
118
|
+
if ((pagesText === null) !== (routesText === null)) {
|
|
119
|
+
throw new ToolError('routes_incomplete', `Found only one of ${p.pages} and ${p.routes}.`, 'codz needs both (the GetX CLI layout), or neither so it can create them.');
|
|
120
|
+
}
|
|
121
|
+
const k = routeConst(opts.name, false);
|
|
122
|
+
let pages = [
|
|
123
|
+
"part 'app_routes.dart';",
|
|
124
|
+
'',
|
|
125
|
+
'class AppPages {',
|
|
126
|
+
' AppPages._();',
|
|
127
|
+
'',
|
|
128
|
+
` static const initial = Routes.${k};`,
|
|
129
|
+
'',
|
|
130
|
+
' static final routes = [',
|
|
131
|
+
getPageEntry(p, k, ' '),
|
|
132
|
+
' ];',
|
|
133
|
+
'}',
|
|
134
|
+
].join('\n') + '\n';
|
|
135
|
+
pages = addDartImport(pages, importUri(p.pages, p.view, c));
|
|
136
|
+
pages = addDartImport(pages, importUri(p.pages, p.binding, c));
|
|
137
|
+
pages = addDartImport(pages, 'package:get/get.dart');
|
|
138
|
+
files.set(p.pages, pages);
|
|
139
|
+
files.set(p.routes, [
|
|
140
|
+
"part of 'app_pages.dart';",
|
|
141
|
+
'',
|
|
142
|
+
'abstract class Routes {',
|
|
143
|
+
' Routes._();',
|
|
144
|
+
` static const ${k} = _Paths.${k};`,
|
|
145
|
+
'}',
|
|
146
|
+
'',
|
|
147
|
+
'abstract class _Paths {',
|
|
148
|
+
' _Paths._();',
|
|
149
|
+
` static const ${k} = '${routePath}';`,
|
|
150
|
+
'}',
|
|
151
|
+
'',
|
|
152
|
+
].join('\n'));
|
|
153
|
+
summary.push(`Create ${p.pages}: AppPages with the ${routePath} page, the first screen`, `Create ${p.routes}: Routes.${k} and its path`);
|
|
154
|
+
// Point the app at the routes.
|
|
155
|
+
const main = read('lib/main.dart');
|
|
156
|
+
if (main === null)
|
|
157
|
+
warnings.push('lib/main.dart was not found; use GetMaterialApp(initialRoute: AppPages.initial, getPages: AppPages.routes).');
|
|
158
|
+
else {
|
|
159
|
+
const wired = wireRoutesInMain(main, c, warnings);
|
|
160
|
+
if (wired !== null) {
|
|
161
|
+
files.set('lib/main.dart', withEol(wired, detectEol(main)));
|
|
162
|
+
summary.push('Update lib/main.dart: GetMaterialApp uses initialRoute and getPages');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const upper = /static\s+const\s+[A-Z][A-Z0-9_]*\s*=/.test(routesText);
|
|
168
|
+
const k = routeConst(opts.name, upper);
|
|
169
|
+
if (new RegExp(`static\\s+const\\s+${k}\\s*=`).test(routesText)) {
|
|
170
|
+
throw new ToolError('route_exists', `The route constant ${k} already exists in ${p.routes}.`, 'Pick another module name.');
|
|
171
|
+
}
|
|
172
|
+
let routes = routesText.replace(/\r\n/g, '\n');
|
|
173
|
+
routes = insertInClass(routes, 'Routes', `static const ${k} = _Paths.${k};`);
|
|
174
|
+
routes = insertInClass(routes, '_Paths', `static const ${k} = '${routePath}';`);
|
|
175
|
+
files.set(p.routes, withEol(routes, detectEol(routesText)));
|
|
176
|
+
let pages = pagesText.replace(/\r\n/g, '\n');
|
|
177
|
+
const listStart = /\broutes\s*=\s*(?:<GetPage[^>]*>\s*)?\[/.exec(pages);
|
|
178
|
+
if (!listStart)
|
|
179
|
+
throw new ToolError('routes_unreadable', `Could not find the routes list in ${p.pages}.`, 'codz expects: static final routes = [ GetPage(...), ... ];');
|
|
180
|
+
const open = listStart.index + listStart[0].length - 1;
|
|
181
|
+
const close = matchBrace(pages, open, 'dart');
|
|
182
|
+
const lineStart = pages.lastIndexOf('\n', listStart.index) + 1;
|
|
183
|
+
const indent = pages.slice(lineStart).match(/^[ \t]*/)[0];
|
|
184
|
+
let body = pages.slice(open + 1, close).replace(/\s*$/, '');
|
|
185
|
+
if (body.trim() && !body.trim().endsWith(','))
|
|
186
|
+
body += ',';
|
|
187
|
+
pages = `${pages.slice(0, open + 1)}${body}\n${getPageEntry(p, k, indent + ' ')}\n${indent}${pages.slice(close)}`;
|
|
188
|
+
if (opts.initial) {
|
|
189
|
+
const ini = /(static\s+const\s+(?:INITIAL|initial)\s*=\s*)Routes\.\w+/.exec(pages);
|
|
190
|
+
if (ini)
|
|
191
|
+
pages = pages.replace(ini[0], `${ini[1]}Routes.${k}`);
|
|
192
|
+
else
|
|
193
|
+
warnings.push(`No initial route constant was found in ${p.pages}; set the first screen yourself.`);
|
|
194
|
+
}
|
|
195
|
+
pages = addDartImport(pages, importUri(p.pages, p.binding, c));
|
|
196
|
+
pages = addDartImport(pages, importUri(p.pages, p.view, c));
|
|
197
|
+
files.set(p.pages, withEol(pages, detectEol(pagesText)));
|
|
198
|
+
summary.push(`Update ${p.routes}: Routes.${k} = '${routePath}'`, `Update ${p.pages}: GetPage for ${p.pascal}View${opts.initial ? ', now the first screen' : ''}`);
|
|
199
|
+
}
|
|
200
|
+
// Only report files that changed relative to the overlay passed in.
|
|
201
|
+
return { files, summary, warnings, generated: [{ skill: 'module add', file: p.controller, symbol: `${p.pascal}Controller`, inputHash: '' }] };
|
|
202
|
+
}
|
|
203
|
+
function wireRoutesInMain(main, c, warnings) {
|
|
204
|
+
let t = main.replace(/\r\n/g, '\n');
|
|
205
|
+
if (/\bgetPages\s*:/.test(t))
|
|
206
|
+
return null;
|
|
207
|
+
const m = /\bGetMaterialApp\s*\(/.exec(t);
|
|
208
|
+
if (!m) {
|
|
209
|
+
warnings.push('lib/main.dart has no GetMaterialApp; use GetMaterialApp(initialRoute: AppPages.initial, getPages: AppPages.routes).');
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const open = m.index + m[0].length - 1;
|
|
213
|
+
const close = matchBrace(t, open, 'dart');
|
|
214
|
+
const lineStart = t.lastIndexOf('\n', m.index) + 1;
|
|
215
|
+
const indent = t.slice(lineStart).match(/^[ \t]*/)[0];
|
|
216
|
+
let args = t.slice(open + 1, close);
|
|
217
|
+
// `home:` would win over the routes; remove it when it is a simple one-line argument.
|
|
218
|
+
const home = /\n([ \t]*)home:\s*([^\n]*),[ \t]*(?=\n)/.exec(args);
|
|
219
|
+
if (home) {
|
|
220
|
+
const value = home[2];
|
|
221
|
+
const balanced = (value.match(/\(/g) ?? []).length === (value.match(/\)/g) ?? []).length;
|
|
222
|
+
if (balanced) {
|
|
223
|
+
args = args.replace(home[0], '');
|
|
224
|
+
warnings.push(`Removed "home: ${value}" from GetMaterialApp: the app now starts at AppPages.initial. The old home widget is still in main.dart if you want to reuse it.${/\bMyHomePage\b/.test(value) ? ' The counter test from flutter create (test/widget_test.dart) checks that page and will fail now; update it or delete it.' : ''}`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
warnings.push('GetMaterialApp has a multi-line home: argument; remove it so the app starts at AppPages.initial.');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const insert = `\n${indent} initialRoute: AppPages.initial,\n${indent} getPages: AppPages.routes,`;
|
|
231
|
+
// After initialBinding when present, else first.
|
|
232
|
+
const ib = /\n[ \t]*initialBinding:[^\n]*,/.exec(args);
|
|
233
|
+
args = ib ? args.slice(0, ib.index + ib[0].length) + insert + args.slice(ib.index + ib[0].length) : insert + args;
|
|
234
|
+
t = t.slice(0, open + 1) + args + t.slice(close);
|
|
235
|
+
return addDartImport(t, importUri('lib/main.dart', c.routesFile || 'lib/app/routes/app_pages.dart', c));
|
|
236
|
+
}
|
|
237
|
+
// ---------------- api wire ----------------
|
|
238
|
+
/** Splits a parameter list at top-level commas. */
|
|
239
|
+
export function splitParams(s) {
|
|
240
|
+
const out = [];
|
|
241
|
+
let depth = 0;
|
|
242
|
+
let cur = '';
|
|
243
|
+
for (const ch of s) {
|
|
244
|
+
if ('<({['.includes(ch))
|
|
245
|
+
depth++;
|
|
246
|
+
if ('>)}]'.includes(ch))
|
|
247
|
+
depth--;
|
|
248
|
+
if (ch === ',' && depth === 0) {
|
|
249
|
+
if (cur.trim())
|
|
250
|
+
out.push(cur.trim());
|
|
251
|
+
cur = '';
|
|
252
|
+
}
|
|
253
|
+
else
|
|
254
|
+
cur += ch;
|
|
255
|
+
}
|
|
256
|
+
if (cur.trim())
|
|
257
|
+
out.push(cur.trim());
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
export function readMethod(text, method) {
|
|
261
|
+
const re = new RegExp(`Future<([^\\n]*)>\\s+${method}\\s*\\(`);
|
|
262
|
+
const m = re.exec(text);
|
|
263
|
+
if (!m)
|
|
264
|
+
return null;
|
|
265
|
+
const open = m.index + m[0].length - 1;
|
|
266
|
+
const close = matchBrace(text, open, 'dart');
|
|
267
|
+
const params = text.slice(open + 1, close).trim().replace(/,\s*$/, '');
|
|
268
|
+
// Which class holds it?
|
|
269
|
+
let repoClass = '';
|
|
270
|
+
for (const cm of text.matchAll(/\bclass\s+(\w+)[^{]*\{/g)) {
|
|
271
|
+
const o = cm.index + cm[0].length - 1;
|
|
272
|
+
const cl = matchBrace(text, o, 'dart');
|
|
273
|
+
if (o < m.index && m.index < cl)
|
|
274
|
+
repoClass = cm[1];
|
|
275
|
+
}
|
|
276
|
+
const positional = [];
|
|
277
|
+
const named = [];
|
|
278
|
+
let hasRequired = false;
|
|
279
|
+
const nameOf = (p) => p.replace(/=.*$/, '').trim().split(/\s+/).pop().replace(/^this\./, '');
|
|
280
|
+
const types = [m[1]];
|
|
281
|
+
for (const part of splitParams(params)) {
|
|
282
|
+
if (part.startsWith('{') || part.startsWith('[')) {
|
|
283
|
+
const optional = part.startsWith('[');
|
|
284
|
+
for (const np of splitParams(part.slice(1, -1))) {
|
|
285
|
+
if (optional)
|
|
286
|
+
positional.push(nameOf(np));
|
|
287
|
+
else {
|
|
288
|
+
named.push(nameOf(np));
|
|
289
|
+
if (/^required\s/.test(np))
|
|
290
|
+
hasRequired = true;
|
|
291
|
+
}
|
|
292
|
+
types.push(np);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
positional.push(nameOf(part));
|
|
297
|
+
hasRequired = true;
|
|
298
|
+
types.push(part);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const args = [...positional, ...named.map((n) => `${n}: ${n}`)].join(', ');
|
|
302
|
+
return { repoClass, returnType: m[1].trim(), params, args, hasRequired, types };
|
|
303
|
+
}
|
|
304
|
+
export function planGetxWire(ctx, opts, overlay = new Map()) {
|
|
305
|
+
const { root, map } = ctx;
|
|
306
|
+
const c = map.conventions;
|
|
307
|
+
requireGetx(c);
|
|
308
|
+
const files = new Map(overlay);
|
|
309
|
+
const read = (path) => files.get(path) ?? readText(join(root, path));
|
|
310
|
+
const summary = [];
|
|
311
|
+
const warnings = [];
|
|
312
|
+
const ep = map.endpoints.find((e) => e.name === opts.endpoint) ?? map.endpoints.find((e) => e.name.toLowerCase() === opts.endpoint.toLowerCase());
|
|
313
|
+
if (!ep) {
|
|
314
|
+
const names = map.endpoints.map((e) => e.name);
|
|
315
|
+
throw new ToolError('no_endpoint', `No endpoint called ${opts.endpoint} was found.`, names.length ? `Endpoints in this project: ${names.join(', ')}` : 'Add one first with: codz api add');
|
|
316
|
+
}
|
|
317
|
+
const repoText = read(ep.file);
|
|
318
|
+
const sig = repoText && readMethod(repoText, ep.name);
|
|
319
|
+
if (!sig || !sig.repoClass)
|
|
320
|
+
throw new ToolError('no_endpoint', `Could not read ${ep.name} in ${ep.file}.`);
|
|
321
|
+
const p = getxPaths(c, opts.module);
|
|
322
|
+
const ctrlText = read(p.controller);
|
|
323
|
+
if (ctrlText === null) {
|
|
324
|
+
throw new ToolError('no_module', `The ${p.snake} module was not found (${p.controller}).`, `Create it with: codz module add ${p.snake}`);
|
|
325
|
+
}
|
|
326
|
+
const ctrlClass = `${p.pascal}Controller`;
|
|
327
|
+
if (!new RegExp(`\\bclass\\s+${ctrlClass}\\b`).test(ctrlText))
|
|
328
|
+
throw new ToolError('class_not_found', `${p.controller} does not define ${ctrlClass}.`);
|
|
329
|
+
// Names: getUser → loadUser / user; createOrder → createOrder / createOrderResult.
|
|
330
|
+
const isRead = ep.method === 'GET';
|
|
331
|
+
const stripped = ep.name.replace(/^(get|fetch|load)(?=[A-Z])/, '');
|
|
332
|
+
const baseName = isRead && stripped !== ep.name ? camel(stripped) : ep.name;
|
|
333
|
+
const action = isRead ? `load${pascal(baseName)}` : ep.name;
|
|
334
|
+
const rt = sig.returnType;
|
|
335
|
+
const isVoid = rt === 'void';
|
|
336
|
+
const list = /^List<(.+)>$/.exec(rt);
|
|
337
|
+
const dataField = isVoid ? '' : isRead ? baseName : `${baseName}Result`;
|
|
338
|
+
const loading = `is${pascal(baseName)}Loading`;
|
|
339
|
+
const errorField = `${baseName}Error`;
|
|
340
|
+
const marker = `// ${ep.method} ${ep.path} via ${sig.repoClass}.${ep.name}`;
|
|
341
|
+
if (ctrlText.includes(marker)) {
|
|
342
|
+
return { files: new Map(overlay), summary: [`${ep.name} is already wired into ${ctrlClass}; nothing to do.`], warnings: [], generated: [] };
|
|
343
|
+
}
|
|
344
|
+
for (const name of [action, dataField, loading, errorField].filter(Boolean)) {
|
|
345
|
+
if (new RegExp(`\\b${name}\\b`).test(ctrlText.replace(/\/\/.*$/gm, ''))) {
|
|
346
|
+
throw new ToolError('name_conflict', `${ctrlClass} already has a member called ${name}.`, 'Rename it, or wire the endpoint into another module.');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Repository field, reused when the controller already has one.
|
|
350
|
+
let text = ctrlText.replace(/\r\n/g, '\n');
|
|
351
|
+
const existingField = new RegExp(`\\b${sig.repoClass}\\s+(\\w+)\\s*=`).exec(text);
|
|
352
|
+
const repoField = existingField ? existingField[1] : `_${camel(sig.repoClass)}`;
|
|
353
|
+
const L = [` ${marker}`];
|
|
354
|
+
if (!isVoid)
|
|
355
|
+
L.push(list ? ` final ${dataField} = <${list[1]}>[].obs;` : ` final ${dataField} = Rxn<${rt}>();`);
|
|
356
|
+
L.push(` final ${loading} = false.obs;`, ` final ${errorField} = RxnString();`, '');
|
|
357
|
+
L.push(` Future<void> ${action}(${sig.params}) async {`, ` ${loading}.value = true;`, ` ${errorField}.value = null;`, ' try {');
|
|
358
|
+
const call = `await ${repoField}.${ep.name}(${sig.args})`;
|
|
359
|
+
if (isVoid)
|
|
360
|
+
L.push(` ${call};`);
|
|
361
|
+
else if (list)
|
|
362
|
+
L.push(` ${dataField}.assignAll(${call});`);
|
|
363
|
+
else
|
|
364
|
+
L.push(` ${dataField}.value = ${call};`);
|
|
365
|
+
L.push(' } catch (e) {', ` ${errorField}.value = e.toString();`, ' } finally {', ` ${loading}.value = false;`, ' }', ' }');
|
|
366
|
+
text = addToClass(text, ctrlClass, L.join('\n'));
|
|
367
|
+
if (!existingField) {
|
|
368
|
+
const head = new RegExp(`(\\bclass\\s+${ctrlClass}\\b[^{]*\\{)\\n*`).exec(text);
|
|
369
|
+
text = `${text.slice(0, head.index)}${head[1]}\n final ${sig.repoClass} ${repoField} = Get.find<${sig.repoClass}>();\n\n${text.slice(head.index + head[0].length)}`;
|
|
370
|
+
}
|
|
371
|
+
text = text.replace(new RegExp(`(\\bclass\\s+${ctrlClass}\\b[^{]*\\{)\\n\\n`), '$1\n');
|
|
372
|
+
// Load on start when the call needs no arguments.
|
|
373
|
+
const autoLoad = isRead && !sig.hasRequired && !opts.noLoad;
|
|
374
|
+
if (autoLoad) {
|
|
375
|
+
const onInit = /void\s+onInit\s*\(\s*\)\s*\{/.exec(text);
|
|
376
|
+
if (onInit) {
|
|
377
|
+
const close = matchBrace(text, onInit.index + onInit[0].length - 1, 'dart');
|
|
378
|
+
const before = text.slice(0, close).replace(/\s*$/, '');
|
|
379
|
+
text = `${before}\n ${action}();\n ${text.slice(close)}`;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
text = addToClass(text, ctrlClass, [' @override', ' void onInit() {', ' super.onInit();', ` ${action}();`, ' }'].join('\n'));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// Imports: Get, the repository, and models in the signature.
|
|
386
|
+
text = addDartImport(text, 'package:get/get.dart');
|
|
387
|
+
text = addDartImport(text, importUri(p.controller, ep.file, c));
|
|
388
|
+
const typeNames = new Set(sig.types.join(' ').match(/\b[A-Z]\w*/g) ?? []);
|
|
389
|
+
// Models already in the project, or created earlier in this same change.
|
|
390
|
+
const modelFileOf = (t) => map.models.find((m) => m.name === t)?.file ??
|
|
391
|
+
[...files.entries()].find(([f, txt]) => f.startsWith((c.modelsFolder || 'lib') + '/') && f.endsWith('.dart') && new RegExp(`\\bclass\\s+${t}\\b`).test(txt))?.[0];
|
|
392
|
+
for (const t of [...typeNames].sort()) {
|
|
393
|
+
const mf = modelFileOf(t);
|
|
394
|
+
if (mf)
|
|
395
|
+
text = addDartImport(text, importUri(p.controller, mf, c));
|
|
396
|
+
}
|
|
397
|
+
files.set(p.controller, withEol(text, detectEol(ctrlText)));
|
|
398
|
+
summary.push(`Update ${p.controller}: ${action}()${dataField ? `, ${dataField}` : ''}, ${loading} and ${errorField}${autoLoad ? '; loads in onInit' : ''}`);
|
|
399
|
+
// Make sure the repository can be found.
|
|
400
|
+
const diText = c.diFile ? read(c.diFile) : null;
|
|
401
|
+
const registered = new RegExp(`Get\\.(?:lazyPut|put|putAsync|create)\\s*<\\s*${sig.repoClass}\\s*>`);
|
|
402
|
+
const bindingText = read(p.binding);
|
|
403
|
+
if (!(diText && registered.test(diText)) && !(bindingText && registered.test(bindingText))) {
|
|
404
|
+
if (bindingText && repoText && new RegExp(`${sig.repoClass}\\s*\\(\\s*this\\.\\w+\\s*\\)`).test(repoText) && /\bDio\b/.test(repoText)) {
|
|
405
|
+
let b = insertGetxRegistration(bindingText.replace(/\r\n/g, '\n'), `Get.lazyPut<${sig.repoClass}>(() => ${sig.repoClass}(Get.find<Dio>()));`);
|
|
406
|
+
if (b) {
|
|
407
|
+
b = addDartImport(b, 'package:dio/dio.dart');
|
|
408
|
+
b = addDartImport(b, importUri(p.binding, ep.file, c));
|
|
409
|
+
files.set(p.binding, withEol(b, detectEol(bindingText)));
|
|
410
|
+
summary.push(`Update ${p.binding}: register ${sig.repoClass} for this module`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
warnings.push(`${sig.repoClass} is not registered with GetX; register it (Get.lazyPut) so Get.find<${sig.repoClass}>() works.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const show = isVoid ? '' : list ? `controller.${dataField}.length` : `controller.${dataField}.value`;
|
|
418
|
+
summary.push(`In ${p.pascal}View: Obx(() => controller.${loading}.value ? const CircularProgressIndicator() : Text(controller.${errorField}.value ?? ${show ? `'\${${show}}'` : "'Done'"}))`);
|
|
419
|
+
if (!autoLoad && isRead)
|
|
420
|
+
summary.push(`Call controller.${action}(${sig.args.replace(/(\w+): \w+/g, '$1: ...')}) when the screen needs the data.`);
|
|
421
|
+
// Return only files that differ from the overlay given.
|
|
422
|
+
return { files, summary, warnings, generated: [{ skill: 'api wire', file: p.controller, symbol: action, inputHash: '' }] };
|
|
423
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { loadConfig } from "../core/config.js";
|
|
2
|
+
import { detectAt } from "../detector.js";
|
|
3
|
+
import { flutterAdapter } from "./flutter.js";
|
|
4
|
+
import { genericAdapter } from "./generic.js";
|
|
5
|
+
import { nextjsAdapter } from "./nextjs.js";
|
|
6
|
+
import { reactNativeAdapter } from "./reactNative.js";
|
|
7
|
+
import { androidAdapter } from "./android.js";
|
|
8
|
+
import { reactAdapter } from "./react.js";
|
|
9
|
+
import { iosAdapter } from "./ios.js";
|
|
10
|
+
export const ADAPTERS = {
|
|
11
|
+
nextjs: nextjsAdapter,
|
|
12
|
+
flutter: flutterAdapter,
|
|
13
|
+
'react-native': reactNativeAdapter,
|
|
14
|
+
react: reactAdapter,
|
|
15
|
+
ios: iosAdapter,
|
|
16
|
+
android: androidAdapter,
|
|
17
|
+
generic: genericAdapter,
|
|
18
|
+
};
|
|
19
|
+
/** Picks the adapter: the framework in codz.config.yaml wins, then detection. */
|
|
20
|
+
export function adapterFor(root) {
|
|
21
|
+
const cfg = loadConfig(root);
|
|
22
|
+
if (cfg.framework && ADAPTERS[cfg.framework])
|
|
23
|
+
return { adapter: ADAPTERS[cfg.framework], label: cfg.framework };
|
|
24
|
+
const d = detectAt(root);
|
|
25
|
+
if (d?.supported)
|
|
26
|
+
return { adapter: ADAPTERS[d.framework], label: d.label };
|
|
27
|
+
return { adapter: genericAdapter, label: d?.label ?? 'unknown' };
|
|
28
|
+
}
|