codz-cli 0.0.0-stage → 0.1.2
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 +525 -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 +638 -0
- package/dist/skills/createDemo.js +1026 -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,252 @@
|
|
|
1
|
+
// Flutter `setup`: creates an API layer for a project that has none yet.
|
|
2
|
+
// Three architectures, each wired end to end so `api add` works right after:
|
|
3
|
+
// getx Dio registered in an InitialBinding, used by GetMaterialApp
|
|
4
|
+
// riverpod Dio exposed through dioProvider, app wrapped in ProviderScope
|
|
5
|
+
// get_it Dio registered in setupDependencies(), called from main()
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { detectEol, readText, walk, withEol } from "../core/fsutil.js";
|
|
8
|
+
import { ToolError } from "../core/types.js";
|
|
9
|
+
import { addDartImport, importUri, pubspecDeps } from "./flutter.js";
|
|
10
|
+
import { matchBrace } from "./source.js";
|
|
11
|
+
/**
|
|
12
|
+
* Lowest versions known to work with the generated code; pub resolves the
|
|
13
|
+
* newest compatible release within each major version.
|
|
14
|
+
*/
|
|
15
|
+
const PACKAGES = {
|
|
16
|
+
dio: '^5.7.0',
|
|
17
|
+
get: '^4.6.6',
|
|
18
|
+
flutter_riverpod: '^2.6.1',
|
|
19
|
+
get_it: '^8.0.2',
|
|
20
|
+
flutter_bloc: '^8.1.6',
|
|
21
|
+
};
|
|
22
|
+
const LAYOUTS = {
|
|
23
|
+
getx: {
|
|
24
|
+
packages: ['dio', 'get'],
|
|
25
|
+
networkFile: 'lib/app/data/network/api_client.dart',
|
|
26
|
+
conventions: {
|
|
27
|
+
modelsFolder: 'lib/app/data/models',
|
|
28
|
+
repoFolder: 'lib/app/data/repositories',
|
|
29
|
+
di: 'getx',
|
|
30
|
+
diFile: 'lib/app/bindings/initial_binding.dart',
|
|
31
|
+
stateManagement: 'getx',
|
|
32
|
+
modulesFolder: 'lib/app/modules',
|
|
33
|
+
routesFile: 'lib/app/routes/app_pages.dart',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
riverpod: {
|
|
37
|
+
packages: ['dio', 'flutter_riverpod'],
|
|
38
|
+
networkFile: 'lib/core/network/dio_provider.dart',
|
|
39
|
+
conventions: {
|
|
40
|
+
modelsFolder: 'lib/data/models',
|
|
41
|
+
repoFolder: 'lib/data/repositories',
|
|
42
|
+
di: 'riverpod',
|
|
43
|
+
dioProvider: 'dioProvider',
|
|
44
|
+
dioProviderFile: 'lib/core/network/dio_provider.dart',
|
|
45
|
+
stateManagement: 'riverpod',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
get_it: {
|
|
49
|
+
packages: ['dio', 'get_it'],
|
|
50
|
+
networkFile: 'lib/core/network/api_client.dart',
|
|
51
|
+
conventions: {
|
|
52
|
+
modelsFolder: 'lib/data/models',
|
|
53
|
+
repoFolder: 'lib/data/repositories',
|
|
54
|
+
di: 'get_it',
|
|
55
|
+
diFile: 'lib/core/di/injection.dart',
|
|
56
|
+
diInstance: 'getIt',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
// Bloc: get_it for Dio and repositories, flutter_bloc for screens (screen add writes Cubits).
|
|
61
|
+
LAYOUTS.bloc = { ...LAYOUTS.get_it, packages: ['dio', 'get_it', 'flutter_bloc'], conventions: { ...LAYOUTS.get_it.conventions, stateManagement: 'bloc' } };
|
|
62
|
+
export function flutterArchitectures(map) {
|
|
63
|
+
const c = map.conventions;
|
|
64
|
+
if (map.endpoints.length > 0 || (c.di && c.di !== 'none'))
|
|
65
|
+
return [];
|
|
66
|
+
const sm = c.stateManagement ?? 'none';
|
|
67
|
+
const recommended = sm === 'getx' ? 'getx' : sm === 'bloc' ? 'bloc' : sm === 'provider' ? 'get_it' : 'riverpod';
|
|
68
|
+
const options = [
|
|
69
|
+
{ id: 'getx', label: 'GetX', description: 'Dio registered in an InitialBinding; repositories with Get.lazyPut; GetMaterialApp', recommended: false },
|
|
70
|
+
{ id: 'riverpod', label: 'Riverpod', description: 'Dio through dioProvider; a provider per repository; ProviderScope', recommended: false },
|
|
71
|
+
{ id: 'get_it', label: 'get_it', description: 'Service locator for Dio and repositories; works with Bloc or Provider', recommended: false },
|
|
72
|
+
{ id: 'bloc', label: 'Bloc', description: 'flutter_bloc for screens (Cubits), get_it for Dio and repositories', recommended: false },
|
|
73
|
+
];
|
|
74
|
+
for (const o of options)
|
|
75
|
+
o.recommended = o.id === recommended;
|
|
76
|
+
return options.sort((a, b) => Number(b.recommended) - Number(a.recommended));
|
|
77
|
+
}
|
|
78
|
+
function networkFile(arch, baseUrl) {
|
|
79
|
+
const L = [
|
|
80
|
+
"import 'package:dio/dio.dart';",
|
|
81
|
+
"import 'package:flutter/foundation.dart';",
|
|
82
|
+
];
|
|
83
|
+
if (arch === 'riverpod')
|
|
84
|
+
L.push("import 'package:flutter_riverpod/flutter_riverpod.dart';");
|
|
85
|
+
L.push('', '/// Creates the Dio instance that every repository uses.', '///', '/// Add auth headers or token refresh here with an interceptor.', 'Dio createDio() {', ' final dio = Dio(', ' BaseOptions(', ` baseUrl: '${baseUrl.replace(/'/g, "\\'")}',`, ' connectTimeout: const Duration(seconds: 20),', ' receiveTimeout: const Duration(seconds: 20),', " headers: {'Accept': 'application/json'},", ' ),', ' );', ' if (kDebugMode) {', ' dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true));', ' }', ' return dio;', '}');
|
|
86
|
+
if (arch === 'riverpod')
|
|
87
|
+
L.push('', 'final dioProvider = Provider<Dio>((ref) => createDio());');
|
|
88
|
+
return L.join('\n') + '\n';
|
|
89
|
+
}
|
|
90
|
+
/** Adds missing packages under `dependencies:`, after the Flutter SDK entry. */
|
|
91
|
+
export function addPubspecDeps(text, packages) {
|
|
92
|
+
const have = pubspecDeps(text);
|
|
93
|
+
const missing = packages.filter((p) => !have.has(p)).sort();
|
|
94
|
+
if (!missing.length)
|
|
95
|
+
return { text, added: [] };
|
|
96
|
+
const eol = detectEol(text);
|
|
97
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
98
|
+
const depIdx = lines.findIndex((l) => /^dependencies:\s*$/.test(l));
|
|
99
|
+
const newLines = missing.map((p) => ` ${p}: ${PACKAGES[p] ?? 'any'}`);
|
|
100
|
+
if (depIdx === -1) {
|
|
101
|
+
lines.push('', 'dependencies:', ...newLines);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
let at = depIdx + 1;
|
|
105
|
+
// Keep `flutter: sdk: flutter` first.
|
|
106
|
+
if (/^ {2}flutter:\s*$/.test(lines[at] ?? '') && /^ {4}sdk:\s*flutter/.test(lines[at + 1] ?? ''))
|
|
107
|
+
at += 2;
|
|
108
|
+
// Then skip any other SDK packages (flutter_localizations etc.) so the new ones follow them.
|
|
109
|
+
while (/^ {2}\w+:\s*$/.test(lines[at] ?? '') && /^ {4}sdk:/.test(lines[at + 1] ?? ''))
|
|
110
|
+
at += 2;
|
|
111
|
+
lines.splice(at, 0, ...newLines);
|
|
112
|
+
}
|
|
113
|
+
return { text: withEol(lines.join('\n'), eol), added: missing };
|
|
114
|
+
}
|
|
115
|
+
function wireMain(arch, main, c, warnings) {
|
|
116
|
+
let t = main.replace(/\r\n/g, '\n');
|
|
117
|
+
if (arch === 'getx') {
|
|
118
|
+
if (/initialBinding\s*:/.test(t))
|
|
119
|
+
return null;
|
|
120
|
+
const m = /\b(?<!Get)MaterialApp(\.router)?\s*\(/.exec(t) ?? /\bGetMaterialApp(\.router)?\s*\(/.exec(t);
|
|
121
|
+
if (!m) {
|
|
122
|
+
warnings.push('lib/main.dart has no MaterialApp, so GetMaterialApp was not wired in. Use GetMaterialApp(initialBinding: InitialBinding(), ...) yourself.');
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const lineStart = t.lastIndexOf('\n', m.index) + 1;
|
|
126
|
+
const indent = t.slice(lineStart).match(/^[ \t]*/)[0];
|
|
127
|
+
const open = m.index + m[0].length;
|
|
128
|
+
const replacement = `${m[0].startsWith('Get') ? m[0] : `Get${m[0]}`}\n${indent} initialBinding: InitialBinding(),`;
|
|
129
|
+
t = t.slice(0, m.index) + replacement + t.slice(open);
|
|
130
|
+
t = addDartImport(t, 'package:get/get.dart');
|
|
131
|
+
t = addDartImport(t, importUri('lib/main.dart', LAYOUTS.getx.conventions.diFile, c));
|
|
132
|
+
return t;
|
|
133
|
+
}
|
|
134
|
+
if (arch === 'riverpod') {
|
|
135
|
+
if (/\bProviderScope\s*\(/.test(t))
|
|
136
|
+
return null;
|
|
137
|
+
const m = /\brunApp\s*\(/.exec(t);
|
|
138
|
+
if (!m) {
|
|
139
|
+
warnings.push('lib/main.dart has no runApp call, so the app was not wrapped in ProviderScope. Do that yourself.');
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const open = m.index + m[0].length - 1;
|
|
143
|
+
const close = matchBrace(t, open, 'dart');
|
|
144
|
+
if (close === -1)
|
|
145
|
+
return null;
|
|
146
|
+
const inner = t.slice(open + 1, close).trim().replace(/,\s*$/, '');
|
|
147
|
+
const wrapped = inner.startsWith('const ') ? `const ProviderScope(child: ${inner.slice(6)})` : `ProviderScope(child: ${inner})`;
|
|
148
|
+
t = `${t.slice(0, open + 1)}${wrapped}${t.slice(close)}`;
|
|
149
|
+
return addDartImport(t, 'package:flutter_riverpod/flutter_riverpod.dart');
|
|
150
|
+
}
|
|
151
|
+
// get_it
|
|
152
|
+
if (/\bsetupDependencies\s*\(/.test(t))
|
|
153
|
+
return null;
|
|
154
|
+
const m = /^([ \t]*)runApp\s*\(/m.exec(t);
|
|
155
|
+
if (!m) {
|
|
156
|
+
warnings.push('lib/main.dart has no runApp call, so setupDependencies() was not called. Call it at the start of main() yourself.');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
t = `${t.slice(0, m.index)}${m[1]}setupDependencies();\n${t.slice(m.index)}`;
|
|
160
|
+
return addDartImport(t, importUri('lib/main.dart', LAYOUTS.get_it.conventions.diFile, c));
|
|
161
|
+
}
|
|
162
|
+
export function planFlutterSetup(ctx, choice) {
|
|
163
|
+
const { root, map } = ctx;
|
|
164
|
+
const layout = LAYOUTS[choice.architecture];
|
|
165
|
+
if (!layout)
|
|
166
|
+
throw new ToolError('bad_args', `Unknown architecture "${choice.architecture}". Choose getx, riverpod, get_it or bloc.`);
|
|
167
|
+
const files = new Map();
|
|
168
|
+
const summary = [];
|
|
169
|
+
const warnings = [];
|
|
170
|
+
const read = (p) => readText(join(root, p));
|
|
171
|
+
const baseUrl = choice.baseUrl || 'https://api.example.com';
|
|
172
|
+
if (!choice.baseUrl)
|
|
173
|
+
warnings.push(`No base URL was given, so ${layout.networkFile} uses https://api.example.com. Change it there before calling the API.`);
|
|
174
|
+
const conventions = {
|
|
175
|
+
...map.conventions,
|
|
176
|
+
http: 'dio',
|
|
177
|
+
serialization: map.conventions.serialization === 'json_serializable' ? 'json_serializable' : 'manual',
|
|
178
|
+
modelFileSuffix: '_model',
|
|
179
|
+
modelClassSuffix: 'Model',
|
|
180
|
+
repoKind: 'repository',
|
|
181
|
+
dioField: '_dio',
|
|
182
|
+
// Keep relative imports when the project already uses them (the GetX CLI does); otherwise package imports.
|
|
183
|
+
importStyle: walk(root, 'lib', { exts: ['.dart'] }).some((f) => /^import\s+['"](?!package:|dart:)/m.test(readText(join(root, f)) ?? '')) ? 'relative' : 'package',
|
|
184
|
+
baseUrl,
|
|
185
|
+
networkFile: layout.networkFile,
|
|
186
|
+
...layout.conventions,
|
|
187
|
+
};
|
|
188
|
+
// Keys that belong to another DI style would confuse later runs.
|
|
189
|
+
for (const k of ['diFile', 'diInstance', 'dioProvider', 'dioProviderFile', 'modulesFolder', 'routesFile'])
|
|
190
|
+
if (!(k in layout.conventions))
|
|
191
|
+
delete conventions[k];
|
|
192
|
+
if (!conventions.packageName)
|
|
193
|
+
throw new ToolError('no_package', 'pubspec.yaml has no name, so package imports cannot be written.');
|
|
194
|
+
const create = (path, content, what) => {
|
|
195
|
+
if (read(path) !== null)
|
|
196
|
+
throw new ToolError('file_exists', `${path} already exists, so setup stopped without changing anything.`, 'Remove or rename it, or set up the project by hand and run codz init again.');
|
|
197
|
+
files.set(path, content);
|
|
198
|
+
summary.push(`Create ${path}: ${what}`);
|
|
199
|
+
};
|
|
200
|
+
create(layout.networkFile, networkFile(choice.architecture, baseUrl), choice.architecture === 'riverpod' ? 'createDio() and dioProvider' : `createDio() with base URL ${baseUrl}`);
|
|
201
|
+
if (choice.architecture === 'getx') {
|
|
202
|
+
let binding = [
|
|
203
|
+
'class InitialBinding extends Bindings {',
|
|
204
|
+
' @override',
|
|
205
|
+
' void dependencies() {',
|
|
206
|
+
' Get.put<Dio>(createDio(), permanent: true);',
|
|
207
|
+
' }',
|
|
208
|
+
'}',
|
|
209
|
+
].join('\n') + '\n';
|
|
210
|
+
binding = addDartImport(binding, 'package:dio/dio.dart');
|
|
211
|
+
binding = addDartImport(binding, 'package:get/get.dart');
|
|
212
|
+
binding = addDartImport(binding, importUri(layout.conventions.diFile, layout.networkFile, conventions));
|
|
213
|
+
create(layout.conventions.diFile, binding, 'InitialBinding that registers Dio; new repositories are added here');
|
|
214
|
+
}
|
|
215
|
+
else if (choice.architecture === 'get_it' || choice.architecture === 'bloc') {
|
|
216
|
+
let di = [
|
|
217
|
+
'final getIt = GetIt.instance;',
|
|
218
|
+
'',
|
|
219
|
+
'/// Registers app-wide dependencies. Called once from main().',
|
|
220
|
+
'void setupDependencies() {',
|
|
221
|
+
' getIt.registerLazySingleton<Dio>(createDio);',
|
|
222
|
+
'}',
|
|
223
|
+
].join('\n') + '\n';
|
|
224
|
+
di = addDartImport(di, 'package:dio/dio.dart');
|
|
225
|
+
di = addDartImport(di, 'package:get_it/get_it.dart');
|
|
226
|
+
di = addDartImport(di, importUri(layout.conventions.diFile, layout.networkFile, conventions));
|
|
227
|
+
create(layout.conventions.diFile, di, 'setupDependencies() that registers Dio; new repositories are added here');
|
|
228
|
+
}
|
|
229
|
+
const main = read('lib/main.dart');
|
|
230
|
+
if (main === null) {
|
|
231
|
+
warnings.push('lib/main.dart was not found, so the app entry point was not wired.');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const wired = wireMain(choice.architecture, main, conventions, warnings);
|
|
235
|
+
if (wired !== null) {
|
|
236
|
+
files.set('lib/main.dart', withEol(wired, detectEol(main)));
|
|
237
|
+
summary.push(`Update lib/main.dart: ${choice.architecture === 'getx' ? 'GetMaterialApp with initialBinding' : choice.architecture === 'riverpod' ? 'wrap the app in ProviderScope' : 'call setupDependencies() before runApp'}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let dependenciesChanged = false;
|
|
241
|
+
const pubspec = read('pubspec.yaml');
|
|
242
|
+
if (pubspec !== null) {
|
|
243
|
+
const { text, added } = addPubspecDeps(pubspec, layout.packages);
|
|
244
|
+
if (added.length) {
|
|
245
|
+
files.set('pubspec.yaml', text);
|
|
246
|
+
summary.push(`Update pubspec.yaml: add ${added.join(', ')}`);
|
|
247
|
+
dependenciesChanged = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
summary.push(`Save conventions: models in ${conventions.modelsFolder}, repositories in ${conventions.repoFolder}, registration with ${choice.architecture}`);
|
|
251
|
+
return { files, summary, warnings, generated: [], conventions, dependenciesChanged };
|
|
252
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Flutter code styles beyond hand-written Dio repositories:
|
|
2
|
+
//
|
|
3
|
+
// freezed models as `@freezed` classes with a const factory constructor
|
|
4
|
+
// (freezed 3 wants `abstract class`, freezed 2 a plain `class`)
|
|
5
|
+
// Retrofit API clients as `@RestApi()` abstract classes whose methods are
|
|
6
|
+
// annotated with the HTTP verb and path; build_runner writes the code
|
|
7
|
+
import { commentText } from "../core/safe.js";
|
|
8
|
+
import { DART_RESERVED, identifier, snake } from "../input/naming.js";
|
|
9
|
+
import { dartType, dq, fieldName } from "./dartGen.js";
|
|
10
|
+
import { matchBrace } from "./source.js";
|
|
11
|
+
// ---------------- freezed ----------------
|
|
12
|
+
export function renderFreezedModel(m, g, fileBase) {
|
|
13
|
+
const cls = g.cls(m.name);
|
|
14
|
+
const kw = g.c.freezedClass === 'plain' ? 'class' : 'abstract class';
|
|
15
|
+
const L = [
|
|
16
|
+
"import 'package:freezed_annotation/freezed_annotation.dart';",
|
|
17
|
+
'',
|
|
18
|
+
`part '${fileBase}.freezed.dart';`,
|
|
19
|
+
`part '${fileBase}.g.dart';`,
|
|
20
|
+
'',
|
|
21
|
+
'@freezed',
|
|
22
|
+
`${kw} ${cls} with _$${cls} {`,
|
|
23
|
+
` const factory ${cls}({`,
|
|
24
|
+
];
|
|
25
|
+
for (const f of m.fields) {
|
|
26
|
+
const name = fieldName(f);
|
|
27
|
+
const unknown = f.type.kind === 'unknown' || (f.type.kind === 'array' && f.type.item.kind === 'unknown');
|
|
28
|
+
if (unknown)
|
|
29
|
+
L.push(' // codz: type unknown from the samples; set it by hand.');
|
|
30
|
+
const key = name !== f.jsonName ? `@JsonKey(name: ${dq(f.jsonName)}) ` : '';
|
|
31
|
+
const optional = f.nullable || f.type.kind === 'unknown';
|
|
32
|
+
const type = dartType(f.type, g) + (f.nullable && f.type.kind !== 'unknown' ? '?' : '');
|
|
33
|
+
L.push(` ${key}${optional ? '' : 'required '}${type} ${name},`);
|
|
34
|
+
}
|
|
35
|
+
L.push(` }) = _${cls};`, '', ` factory ${cls}.fromJson(Map<String, dynamic> json) => _$${cls}FromJson(json);`, '}');
|
|
36
|
+
return L.join('\n');
|
|
37
|
+
}
|
|
38
|
+
/** Splits at top-level commas (outside brackets and strings). */
|
|
39
|
+
function splitTop(s) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let cur = '';
|
|
43
|
+
let quote = '';
|
|
44
|
+
for (let i = 0; i < s.length; i++) {
|
|
45
|
+
const ch = s[i];
|
|
46
|
+
if (quote) {
|
|
47
|
+
cur += ch;
|
|
48
|
+
if (ch === '\\')
|
|
49
|
+
cur += s[++i] ?? '';
|
|
50
|
+
else if (ch === quote)
|
|
51
|
+
quote = '';
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === "'" || ch === '"')
|
|
55
|
+
quote = ch;
|
|
56
|
+
if ('([{<'.includes(ch))
|
|
57
|
+
depth++;
|
|
58
|
+
else if (')]}>'.includes(ch))
|
|
59
|
+
depth--;
|
|
60
|
+
if (ch === ',' && depth === 0) {
|
|
61
|
+
out.push(cur);
|
|
62
|
+
cur = '';
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
cur += ch;
|
|
66
|
+
}
|
|
67
|
+
if (cur.trim())
|
|
68
|
+
out.push(cur);
|
|
69
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
/** Fields of a freezed class, read from its unnamed const factory constructor. */
|
|
72
|
+
export function parseFreezedFields(body, cls) {
|
|
73
|
+
const m = new RegExp(`\\bconst\\s+factory\\s+${cls}\\s*\\(`).exec(body);
|
|
74
|
+
if (!m)
|
|
75
|
+
return null;
|
|
76
|
+
const open = m.index + m[0].length - 1;
|
|
77
|
+
const close = matchBrace(body, open, 'dart');
|
|
78
|
+
if (close === -1)
|
|
79
|
+
return null;
|
|
80
|
+
const inner = body.slice(open + 1, close).trim().replace(/^\{([\s\S]*)\}$/, '$1');
|
|
81
|
+
const fields = [];
|
|
82
|
+
for (const raw of splitTop(inner.replace(/\/\/[^\n]*/g, ''))) {
|
|
83
|
+
const key = raw.match(/@JsonKey\(\s*name:\s*['"]([^'"]+)['"]/)?.[1];
|
|
84
|
+
const rest = raw
|
|
85
|
+
.replace(/@\w+(?:\([^)]*\))?/g, '')
|
|
86
|
+
.replace(/\brequired\b/, '')
|
|
87
|
+
.trim();
|
|
88
|
+
const pm = /^([\w<>?,\s]+?)\s+(\w+)$/.exec(rest);
|
|
89
|
+
if (!pm)
|
|
90
|
+
continue;
|
|
91
|
+
const type = pm[1].replace(/\s+/g, ' ').trim();
|
|
92
|
+
fields.push({ name: key ?? snake(pm[2]), type: type.replace(/\?$/, ''), nullable: type.endsWith('?') || type === 'dynamic' });
|
|
93
|
+
}
|
|
94
|
+
return fields;
|
|
95
|
+
}
|
|
96
|
+
/** Methods of `@RestApi()` classes: `@GET('/users/{id}') Future<User> getUser(...)`. */
|
|
97
|
+
export function scanRetrofit(text) {
|
|
98
|
+
if (!/@RestApi\s*\(/.test(text))
|
|
99
|
+
return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
const re = /@(GET|POST|PUT|PATCH|DELETE|HEAD)\s*\(\s*(['"])([^'"]*)\2\s*\)\s*(?:@\w+(?:\([^)]*\))?\s*)*Future<(.+?)>\s+(\w+)\s*\(/g;
|
|
102
|
+
for (const m of text.matchAll(re))
|
|
103
|
+
out.push({ method: m[1], path: m[3], response: m[4].trim(), name: m[5] });
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
export function renderRetrofitMethod(spec, g, path) {
|
|
107
|
+
const params = spec.pathParams.map((p) => `@Path(${dq(p.name)}) ${p.type === 'int' ? 'int' : 'String'} ${identifier(p.name, DART_RESERVED)}`);
|
|
108
|
+
if (spec.request)
|
|
109
|
+
params.push(`@Body() ${dartType(spec.request, g)} body`);
|
|
110
|
+
const named = spec.queryParams.map((q) => `@Query(${dq(q.name)}) ${dartType(q.type, g)}? ${identifier(q.name, DART_RESERVED)}`);
|
|
111
|
+
const sig = named.length ? `${params.join(', ')}${params.length ? ', ' : ''}{${named.join(', ')}}` : params.join(', ');
|
|
112
|
+
const noBody = spec.method === 'DELETE' || spec.method === 'HEAD';
|
|
113
|
+
const result = noBody ? 'void' : spec.response ? dartType(spec.response, g) : 'dynamic';
|
|
114
|
+
// Retrofit paths are relative to the client's baseUrl; {name} placeholders match @Path names.
|
|
115
|
+
return [` /// ${spec.method} ${commentText(spec.path)}`, ` @${spec.method}(${dq(path)})`, ` Future<${result}> ${spec.name}(${sig});`].join('\n');
|
|
116
|
+
}
|
|
117
|
+
export function newRetrofitClient(cls, fileBase) {
|
|
118
|
+
return [
|
|
119
|
+
"import 'package:dio/dio.dart';",
|
|
120
|
+
"import 'package:retrofit/retrofit.dart';",
|
|
121
|
+
'',
|
|
122
|
+
`part '${fileBase}.g.dart';`,
|
|
123
|
+
'',
|
|
124
|
+
'@RestApi()',
|
|
125
|
+
`abstract class ${cls} {`,
|
|
126
|
+
` factory ${cls}(Dio dio, {String? baseUrl}) = _${cls};`,
|
|
127
|
+
'}',
|
|
128
|
+
'',
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Fallback for projects without a framework adapter: structure, locales and
|
|
2
|
+
// config values only.
|
|
3
|
+
import { walk } from "../core/fsutil.js";
|
|
4
|
+
import { emptyMap, fileStats, listRootEnvFiles, scanEnvBaseUrls, scanLocales } from "./common.js";
|
|
5
|
+
export const genericAdapter = {
|
|
6
|
+
id: 'generic',
|
|
7
|
+
version: '0.1.0',
|
|
8
|
+
capabilities: { explain: true, apiAdd: false },
|
|
9
|
+
supportedSetup: 'Any project: structure, locale files and .env values only.',
|
|
10
|
+
async scan(root, conventions) {
|
|
11
|
+
const map = emptyMap('generic', `generic@${this.version}`);
|
|
12
|
+
const files = walk(root);
|
|
13
|
+
map.stats = fileStats(files);
|
|
14
|
+
map.locales = scanLocales(root, files);
|
|
15
|
+
map.baseUrls = scanEnvBaseUrls(root, listRootEnvFiles(root));
|
|
16
|
+
map.conventions = { ...conventions };
|
|
17
|
+
map.notes.push('No framework adapter matches this project, so only structure, locales and config values are mapped. `api add` needs the Next.js or Flutter adapter.');
|
|
18
|
+
return map;
|
|
19
|
+
},
|
|
20
|
+
};
|