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.
Files changed (118) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +525 -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 +638 -0
  77. package/dist/skills/createDemo.js +1026 -0
  78. package/dist/skills/doctor.js +304 -0
  79. package/dist/skills/doctorFix.js +214 -0
  80. package/dist/skills/explain.js +61 -0
  81. package/dist/skills/feature.js +418 -0
  82. package/dist/skills/featureEdit.js +225 -0
  83. package/dist/skills/featureLinks.js +179 -0
  84. package/dist/skills/init.js +66 -0
  85. package/dist/skills/iosUi.js +468 -0
  86. package/dist/skills/locale.js +335 -0
  87. package/dist/skills/mcp.js +175 -0
  88. package/dist/skills/mock.js +482 -0
  89. package/dist/skills/module.js +23 -0
  90. package/dist/skills/plans.js +66 -0
  91. package/dist/skills/prefill.js +84 -0
  92. package/dist/skills/reactNativeUi.js +388 -0
  93. package/dist/skills/reactUi.js +352 -0
  94. package/dist/skills/report.js +75 -0
  95. package/dist/skills/route.js +230 -0
  96. package/dist/skills/screen.js +597 -0
  97. package/dist/skills/screenForm.js +410 -0
  98. package/dist/skills/setup.js +28 -0
  99. package/dist/skills/setupDoctor.js +226 -0
  100. package/dist/skills/templatesCmd.js +32 -0
  101. package/dist/skills/testgen.js +331 -0
  102. package/dist/skills/theme.js +388 -0
  103. package/dist/starters/auth.js +144 -0
  104. package/dist/starters/commands.js +283 -0
  105. package/dist/starters/engine.js +315 -0
  106. package/dist/starters/home.js +42 -0
  107. package/dist/starters/http.js +93 -0
  108. package/dist/starters/manifest.js +285 -0
  109. package/dist/starters/paths.js +70 -0
  110. package/dist/starters/publish.js +150 -0
  111. package/dist/starters/registry.js +258 -0
  112. package/dist/starters/sign.js +72 -0
  113. package/dist/starters/source.js +98 -0
  114. package/dist/starters/strictJson.js +148 -0
  115. package/dist/starters/tar.js +180 -0
  116. package/docs/COMMANDS.md +527 -0
  117. package/examples/openapi.yaml +319 -0
  118. package/package.json +57 -4
@@ -0,0 +1,109 @@
1
+ // React Native adapter (Expo or the React Native CLI). API code is TypeScript
2
+ // like in Next.js, so scanning and generating endpoints reuse the Next.js
3
+ // adapter: fetch (directly or through a helper), axios, TanStack Query hooks,
4
+ // interfaces for models. On top of that this adapter knows React Native's own
5
+ // parts: the flavor (Expo or bare), navigation (React Navigation or Expo
6
+ // Router), the screens folder, and the config file that holds the API URL.
7
+ import { join } from 'node:path';
8
+ import { exists, isDir, readJson, readText, walk } from "../core/fsutil.js";
9
+ import { NEXT_CONVENTION_DOCS, nextjsAdapter } from "./nextjs.js";
10
+ import { mostCommon } from "./nextjsScan.js";
11
+ import { planReactNativeSetup, reactNativeArchitectures } from "./reactNativeSetup.js";
12
+ export const RN_VERSION = '0.1.0';
13
+ const { appDir: _appDir, ...SHARED_DOCS } = NEXT_CONVENTION_DOCS;
14
+ export const RN_CONVENTION_DOCS = {
15
+ ...SHARED_DOCS,
16
+ flavor: 'expo or bare (React Native CLI)',
17
+ router: 'react-navigation, expo-router or none',
18
+ screensFolder: 'where screen components live',
19
+ navigatorFile: 'the React Navigation stack that lists the screens (react-navigation only)',
20
+ routerDir: 'the Expo Router app folder (expo-router only)',
21
+ configFile: 'file that exports API_URL and USE_MOCKS',
22
+ };
23
+ const NAVIGATOR = /create(?:NativeStack|Stack|BottomTab|Drawer)Navigator\s*(?:<[^>]*>)?\s*\(/;
24
+ /** React Navigation screens: <Stack.Screen name="Orders" component={OrdersScreen} />. */
25
+ function navigationRoutes(file, text) {
26
+ const out = [];
27
+ for (const m of text.matchAll(/<\w+\.Screen\b([^>]*?)\/?>/g)) {
28
+ const name = /\bname\s*=\s*["'{]\s*["']?([\w-]+)/.exec(m[1])?.[1];
29
+ if (!name)
30
+ continue;
31
+ const params = new RegExp(`\\b${name}\\s*:\\s*\\{([^}]*)\\}`).exec(text)?.[1]?.split(/[;,]/).map((p) => p.split(':')[0].replace(/\?$/, '').trim()).filter(Boolean) ?? [];
32
+ out.push({ name, path: name, params, file });
33
+ }
34
+ return out;
35
+ }
36
+ /** Expo Router: every file under app/ except layouts is a route; [id] segments are parameters. */
37
+ function expoRoutes(routerDir, files) {
38
+ return files
39
+ .filter((f) => f.startsWith(`${routerDir}/`) && /\.(tsx|jsx|ts|js)$/.test(f) && !/(^|\/)(_layout|\+[\w-]+)\.\w+$/.test(f))
40
+ .map((f) => {
41
+ const segs = f.slice(routerDir.length + 1).replace(/\.\w+$/, '').split('/').filter((s) => !/^\(.*\)$/.test(s));
42
+ if (segs[segs.length - 1] === 'index')
43
+ segs.pop();
44
+ const params = segs.filter((s) => s.startsWith('[')).map((s) => s.replace(/[[\].]/g, ''));
45
+ const path = '/' + segs.map((s) => (s.startsWith('[') ? `{${s.replace(/[[\].]/g, '')}}` : s)).join('/');
46
+ return { name: segs.filter((s) => !s.startsWith('[')).join('-') || 'home', path: path === '/' ? '/' : path, params, file: f };
47
+ });
48
+ }
49
+ async function scan(root, configConventions) {
50
+ const map = await nextjsAdapter.scan(root, configConventions);
51
+ map.framework = 'react-native';
52
+ map.adapter = `react-native@${RN_VERSION}`;
53
+ map.notes = map.notes.filter((n) => !/app folder|Pages Router/.test(n));
54
+ map.routes = [];
55
+ const pkg = readJson(join(root, 'package.json')) ?? {};
56
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
57
+ const files = walk(root, '.', { exts: ['.ts', '.tsx', '.js', '.jsx'], extraIgnored: ['android', 'ios'] });
58
+ const text = (f) => readText(join(root, f)) ?? '';
59
+ const src = isDir(join(root, 'src')) ? 'src/' : '';
60
+ const c = {};
61
+ c.flavor = deps.expo ? 'expo' : 'bare';
62
+ const routerDir = isDir(join(root, 'src/app')) ? 'src/app' : isDir(join(root, 'app')) ? 'app' : '';
63
+ const navigatorFile = files.find((f) => NAVIGATOR.test(text(f)) && /\.Navigator\b/.test(text(f))) ?? '';
64
+ c.router = deps['expo-router'] && routerDir ? 'expo-router' : deps['@react-navigation/native'] ? 'react-navigation' : 'none';
65
+ c.routerDir = c.router === 'expo-router' ? routerDir : '';
66
+ c.navigatorFile = c.router === 'react-navigation' ? navigatorFile || `${src}navigation/RootNavigator.tsx` : '';
67
+ const screenFiles = files.filter((f) => /Screen\.(tsx|jsx)$/.test(f));
68
+ c.screensFolder = mostCommon(screenFiles.map((f) => f.slice(0, f.lastIndexOf('/'))), `${src}screens`);
69
+ c.configFile = files.find((f) => /export\s+const\s+API_URL\b/.test(text(f))) ?? `${src}config/env.ts`;
70
+ // An API folder that was only guessed follows React Native layouts (src/api), not Next.js (lib/api).
71
+ const guessed = !map.endpoints.length && !configConventions.apiFolder && !exists(join(root, map.conventions.apiFolder ?? ''));
72
+ if (guessed)
73
+ c.apiFolder = `${src}api`;
74
+ if (!map.models.length && !configConventions.modelsFolder && !exists(join(root, map.conventions.modelsFolder ?? '')))
75
+ c.modelsFolder = `${src}types`;
76
+ if (map.conventions.dataFetching === 'react-query' && !configConventions.hooksFolder && !exists(join(root, map.conventions.hooksFolder ?? '')))
77
+ c.hooksFolder = `${src}hooks`;
78
+ const conv = (map.conventions = { ...map.conventions, ...c, ...configConventions });
79
+ delete conv.appDir;
80
+ // The API URL in the config file (a literal, or the fallback after ??) is a base URL too.
81
+ const cfgText = conv.configFile ? readText(join(root, conv.configFile)) ?? '' : '';
82
+ const cfgUrl = /export\s+const\s+API_URL\s*=\s*(?:[^'"\n]*\?\?\s*)?['"](https?:\/\/[^'"]+)['"]/.exec(cfgText)?.[1];
83
+ if (cfgUrl && !map.baseUrls.some((b) => b.value === cfgUrl))
84
+ map.baseUrls.push({ name: 'API_URL', value: cfgUrl, file: conv.configFile });
85
+ if (conv.router === 'expo-router' && conv.routerDir)
86
+ map.routes = expoRoutes(conv.routerDir, files);
87
+ else if (conv.router === 'react-navigation')
88
+ for (const f of files.filter((x) => /\.Screen\b/.test(text(x))))
89
+ map.routes.push(...navigationRoutes(f, text(f)));
90
+ map.routes.sort((a, b) => a.path.localeCompare(b.path));
91
+ if (conv.router === 'none')
92
+ map.notes.push('No navigation library found; screen add and route add need React Navigation or Expo Router (codz setup adds React Navigation).');
93
+ return map;
94
+ }
95
+ export const reactNativeAdapter = {
96
+ id: 'react-native',
97
+ version: RN_VERSION,
98
+ capabilities: { explain: true, apiAdd: true },
99
+ supportedSetup: 'React Native (Expo or React Native CLI), TypeScript, fetch (directly or through a helper) or axios, optionally TanStack Query; React Navigation or Expo Router.',
100
+ scan,
101
+ planEndpoint: nextjsAdapter.planEndpoint,
102
+ architectures: reactNativeArchitectures,
103
+ planSetup: planReactNativeSetup,
104
+ afterSetup(root) {
105
+ return exists(join(root, 'package.json')) ? { cmd: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: ['install'], label: 'npm install' } : null;
106
+ },
107
+ formatCommand: nextjsAdapter.formatCommand,
108
+ verifyCommand: nextjsAdapter.verifyCommand,
109
+ };
@@ -0,0 +1,236 @@
1
+ // React Native `setup`: an API layer (and navigation, when there is none) for
2
+ // a project that has neither yet.
3
+ //
4
+ // fetch-helper src/api/client.ts with apiFetch<T>() and ApiError
5
+ // axios src/api/client.ts exporting an axios instance
6
+ // axios-react-query the axios instance plus TanStack Query (QueryClientProvider)
7
+ //
8
+ // The API URL and the mocks switch live in src/config/env.ts: read from
9
+ // EXPO_PUBLIC_API_URL in Expo, a constant in the React Native CLI (where
10
+ // react-native-config is the usual next step). Without navigation, React
11
+ // Navigation's native stack is added with a Home screen, so screen add works.
12
+ import { join } from 'node:path';
13
+ import { readJson, readText } from "../core/fsutil.js";
14
+ import { ToolError } from "../core/types.js";
15
+ /** Versions known to work with the generated code (React Native 0.76+ / Expo SDK 52+). */
16
+ const PACKAGES = {
17
+ axios: '^1.7.7',
18
+ '@tanstack/react-query': '^5.59.0',
19
+ '@react-navigation/native': '^7.0.0',
20
+ '@react-navigation/native-stack': '^7.0.0',
21
+ 'react-native-screens': '^4.0.0',
22
+ 'react-native-safe-area-context': '^4.12.0',
23
+ };
24
+ export function reactNativeArchitectures(map) {
25
+ const c = map.conventions;
26
+ if (map.endpoints.length > 0 || (c.httpHelper && c.httpHelper !== 'fetch') || c.http === 'axios')
27
+ return [];
28
+ return [
29
+ { id: 'fetch-helper', label: 'Fetch helper', description: 'src/api/client.ts with apiFetch<T>() and ApiError; API URL in src/config/env.ts', recommended: true },
30
+ { id: 'axios', label: 'axios', description: 'An axios instance in src/api/client.ts; API functions call it', recommended: false },
31
+ { id: 'axios-react-query', label: 'axios + TanStack Query', description: 'The axios instance, plus a query hook for every endpoint and QueryClientProvider around the app', recommended: false },
32
+ ];
33
+ }
34
+ function envFile(expo, baseUrl, q, semi) {
35
+ const L = expo
36
+ ? [
37
+ '// App settings. Expo reads EXPO_PUBLIC_* variables from .env at build time.',
38
+ `export const API_URL = process.env.EXPO_PUBLIC_API_URL ?? ${q}${baseUrl}${q}${semi}`,
39
+ '',
40
+ '/** Answer API calls from mocks (codz mock add). Set EXPO_PUBLIC_USE_MOCKS=true in .env. */',
41
+ `export const USE_MOCKS = process.env.EXPO_PUBLIC_USE_MOCKS === ${q}true${q}${semi}`,
42
+ ]
43
+ : [
44
+ '// App settings. For per-build values, move these to react-native-config.',
45
+ `export const API_URL = ${q}${baseUrl}${q}${semi}`,
46
+ '',
47
+ '/** Answer API calls from mocks (codz mock add). */',
48
+ `export const USE_MOCKS = false${semi}`,
49
+ ];
50
+ return L.join('\n') + '\n';
51
+ }
52
+ export function fetchClient(q, semi) {
53
+ return [
54
+ `import { API_URL } from ${q}../config/env${q}${semi}`,
55
+ '',
56
+ '/** Thrown when the API answers with a status outside 200-299. */',
57
+ 'export class ApiError extends Error {',
58
+ ` status: number${semi}`,
59
+ '',
60
+ ' constructor(status: number, message: string) {',
61
+ ` super(message)${semi}`,
62
+ ` this.status = status${semi}`,
63
+ ' }',
64
+ '}',
65
+ '',
66
+ '/** Calls the API and parses the JSON response. Add auth headers here. */',
67
+ 'export async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {',
68
+ ' const res = await fetch(`${API_URL}${path}`, {',
69
+ ' ...init,',
70
+ ` headers: { ${q}Content-Type${q}: ${q}application/json${q}, ...init.headers },`,
71
+ ` })${semi}`,
72
+ ` if (!res.ok) throw new ApiError(res.status, await res.text())${semi}`,
73
+ ` if (res.status === 204) return undefined as T${semi}`,
74
+ ` return (await res.json()) as T${semi}`,
75
+ '}',
76
+ '',
77
+ ].join('\n');
78
+ }
79
+ export function axiosClient(q, semi) {
80
+ return [
81
+ `import axios from ${q}axios${q}${semi}`,
82
+ `import { API_URL } from ${q}../config/env${q}${semi}`,
83
+ '',
84
+ '/** The axios instance every API function uses. Add auth headers with an interceptor. */',
85
+ 'export const api = axios.create({',
86
+ ' baseURL: API_URL,',
87
+ ' timeout: 20000,',
88
+ ` headers: { ${q}Content-Type${q}: ${q}application/json${q} },`,
89
+ `})${semi}`,
90
+ '',
91
+ ].join('\n');
92
+ }
93
+ function navigator(q, semi) {
94
+ return [
95
+ `import { createNativeStackNavigator } from ${q}@react-navigation/native-stack${q}${semi}`,
96
+ `import HomeScreen from ${q}../screens/HomeScreen${q}${semi}`,
97
+ '',
98
+ '/** Every screen and its parameters. codz route add and screen add add entries here. */',
99
+ 'export type RootStackParamList = {',
100
+ ` Home: undefined${semi}`,
101
+ `}${semi}`,
102
+ '',
103
+ `const Stack = createNativeStackNavigator<RootStackParamList>()${semi}`,
104
+ '',
105
+ 'export default function RootNavigator() {',
106
+ ' return (',
107
+ ' <Stack.Navigator>',
108
+ ` <Stack.Screen name=${'"'}Home${'"'} component={HomeScreen} />`,
109
+ ' </Stack.Navigator>',
110
+ ` )${semi}`,
111
+ '}',
112
+ '',
113
+ ].join('\n');
114
+ }
115
+ function homeScreen(semi) {
116
+ return [
117
+ `import { StyleSheet, Text, View } from 'react-native'${semi}`,
118
+ '',
119
+ 'export default function HomeScreen() {',
120
+ ' return (',
121
+ ' <View style={styles.container}>',
122
+ ' <Text>Home</Text>',
123
+ ' </View>',
124
+ ` )${semi}`,
125
+ '}',
126
+ '',
127
+ 'const styles = StyleSheet.create({',
128
+ " container: { flex: 1, alignItems: 'center', justifyContent: 'center' },",
129
+ `})${semi}`,
130
+ '',
131
+ ].join('\n');
132
+ }
133
+ function appFile(nav, query, semi) {
134
+ const imports = [
135
+ ...(nav ? [`import { NavigationContainer } from '@react-navigation/native'${semi}`] : [`import { Text, View } from 'react-native'${semi}`]),
136
+ ...(query ? [`import { QueryClient, QueryClientProvider } from '@tanstack/react-query'${semi}`] : []),
137
+ ...(nav ? [`import RootNavigator from './src/navigation/RootNavigator'${semi}`] : []),
138
+ ];
139
+ let body = nav ? '<NavigationContainer>\n <RootNavigator />\n </NavigationContainer>' : '<View style={{ flex: 1, alignItems: \'center\', justifyContent: \'center\' }}>\n <Text>Home</Text>\n </View>';
140
+ if (query)
141
+ body = `<QueryClientProvider client={queryClient}>\n ${body.replace(/\n/g, '\n ')}\n </QueryClientProvider>`;
142
+ return [...imports, '', ...(query ? [`const queryClient = new QueryClient()${semi}`, ''] : []), 'export default function App() {', ' return (', ` ${body}`, ` )${semi}`, '}', ''].join('\n');
143
+ }
144
+ export function planReactNativeSetup(ctx, choice) {
145
+ const { root, map } = ctx;
146
+ if (!['fetch-helper', 'axios', 'axios-react-query'].includes(choice.architecture))
147
+ throw new ToolError('bad_args', `Unknown architecture "${choice.architecture}". Choose fetch-helper, axios or axios-react-query.`);
148
+ const c0 = map.conventions;
149
+ const expo = c0.flavor === 'expo';
150
+ const src = 'src';
151
+ const q = c0.quote === 'double' ? '"' : "'";
152
+ const semi = c0.semicolons === 'false' ? '' : ';';
153
+ const baseUrl = choice.baseUrl || 'https://api.example.com';
154
+ const files = new Map();
155
+ const rewrites = [];
156
+ const summary = [];
157
+ const warnings = [];
158
+ if (!choice.baseUrl)
159
+ warnings.push(`No base URL was given, so ${src}/config/env.ts uses https://api.example.com. Change it there before calling the API.`);
160
+ const read = (p) => readText(join(root, p));
161
+ const create = (path, content, what) => {
162
+ if (read(path) !== null)
163
+ 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.');
164
+ files.set(path, content);
165
+ summary.push(`Create ${path}: ${what}`);
166
+ };
167
+ const axios = choice.architecture !== 'fetch-helper';
168
+ const query = choice.architecture === 'axios-react-query';
169
+ const nav = c0.router === 'none' || !c0.router;
170
+ create(`${src}/config/env.ts`, envFile(expo, baseUrl, q, semi), expo ? 'API_URL from EXPO_PUBLIC_API_URL, and USE_MOCKS' : 'API_URL and USE_MOCKS');
171
+ create(`${src}/api/client.ts`, axios ? axiosClient(q, semi) : fetchClient(q, semi), axios ? 'the axios instance (baseURL from API_URL)' : 'apiFetch<T>() and ApiError');
172
+ if (!axios)
173
+ create(`${src}/api/index.ts`, `export * from ${q}./client${q}${semi}\n`, 'exports every API file');
174
+ if (expo) {
175
+ const env = read('.env.example');
176
+ files.set('.env.example', `${env ? env.replace(/\s*$/, '\n') : ''}EXPO_PUBLIC_API_URL=${baseUrl}\nEXPO_PUBLIC_USE_MOCKS=false\n`);
177
+ summary.push('Update .env.example: EXPO_PUBLIC_API_URL and EXPO_PUBLIC_USE_MOCKS');
178
+ }
179
+ if (nav) {
180
+ create(`${src}/navigation/RootNavigator.tsx`, navigator(q, semi), 'React Navigation native stack with RootStackParamList');
181
+ create(`${src}/screens/HomeScreen.tsx`, homeScreen(semi), 'the first screen');
182
+ }
183
+ if (nav || query) {
184
+ const app = ['App.tsx', 'App.js'].find((f) => read(f) !== null) ?? 'App.tsx';
185
+ if (c0.router === 'expo-router') {
186
+ warnings.push(`This project uses Expo Router; wrap the root layout in <QueryClientProvider client={new QueryClient()}> yourself.`);
187
+ }
188
+ else {
189
+ files.set(app, appFile(nav, query, semi));
190
+ rewrites.push(app); // replaced on purpose; the previous App is in codz undo
191
+ summary.push(`${read(app) === null ? 'Create' : 'Replace'} ${app}: ${[nav && 'NavigationContainer with RootNavigator', query && 'QueryClientProvider'].filter(Boolean).join(' and ')}${read(app) !== null ? ' (the previous App is in codz undo)' : ''}`);
192
+ }
193
+ }
194
+ // Dependencies.
195
+ const pkgText = read('package.json');
196
+ let dependenciesChanged = false;
197
+ if (pkgText !== null) {
198
+ const pkg = readJson(join(root, 'package.json')) ?? {};
199
+ const want = [...(axios ? ['axios'] : []), ...(query ? ['@tanstack/react-query'] : []), ...(nav ? ['@react-navigation/native', '@react-navigation/native-stack', 'react-native-screens', 'react-native-safe-area-context'] : [])];
200
+ const missing = want.filter((p) => !pkg.dependencies?.[p] && !pkg.devDependencies?.[p]);
201
+ if (missing.length) {
202
+ const parsed = JSON.parse(pkgText);
203
+ parsed.dependencies = { ...parsed.dependencies };
204
+ for (const p of missing)
205
+ parsed.dependencies[p] = PACKAGES[p];
206
+ parsed.dependencies = Object.fromEntries(Object.entries(parsed.dependencies).sort(([a], [b]) => a.localeCompare(b)));
207
+ files.set('package.json', JSON.stringify(parsed, null, 2) + '\n');
208
+ summary.push(`Update package.json: add ${missing.join(', ')}`);
209
+ dependenciesChanged = true;
210
+ if (expo)
211
+ warnings.push('Run npx expo install --fix after installing, so the native packages match your Expo SDK.');
212
+ else if (nav)
213
+ warnings.push('react-native-screens and react-native-safe-area-context are native: run cd ios && pod install after npm install.');
214
+ }
215
+ }
216
+ const conventions = {
217
+ ...c0,
218
+ http: axios ? 'axios' : 'fetch',
219
+ httpHelper: axios ? 'fetch' : 'apiFetch',
220
+ httpHelperFile: axios ? '' : `${src}/api/client.ts`,
221
+ apiFolder: `${src}/api`,
222
+ modelsFolder: `${src}/types`,
223
+ modelsLayout: 'per-file',
224
+ configFile: `${src}/config/env.ts`,
225
+ screensFolder: `${src}/screens`,
226
+ ...(axios ? { axiosInstance: 'api', axiosInstanceFile: `${src}/api/client.ts` } : {}),
227
+ ...(query ? { dataFetching: 'react-query', hooksFolder: `${src}/hooks` } : {}),
228
+ ...(nav ? { router: 'react-navigation', navigatorFile: `${src}/navigation/RootNavigator.tsx`, routerDir: '' } : {}),
229
+ };
230
+ if (!axios)
231
+ for (const k of ['axiosInstance', 'axiosInstanceFile'])
232
+ delete conventions[k];
233
+ delete conventions.baseUrlEnv;
234
+ summary.push(`Save conventions: API functions in ${src}/api, interfaces in ${src}/types, screens in ${src}/screens`);
235
+ return { files, summary, warnings, generated: [], conventions, dependenciesChanged, rewrites };
236
+ }
@@ -0,0 +1,89 @@
1
+ // TanStack Query hooks for Next.js API functions (dataFetching: react-query).
2
+ //
3
+ // GET useQuery with a key of [resource, ...path params, query params]
4
+ // others useMutation whose success invalidates the resource's queries
5
+ //
6
+ // One hooks file per API file: the hooks file that already imports from it,
7
+ // otherwise use-<resource>.ts in the hooks folder.
8
+ import { commentText, jsString } from "../core/safe.js";
9
+ import { pascal } from "../input/naming.js";
10
+ /** Splits a TypeScript parameter list at top-level commas. */
11
+ export function splitTsParams(params) {
12
+ const parts = [];
13
+ let depth = 0;
14
+ let cur = '';
15
+ for (const ch of params) {
16
+ if ('{[(<'.includes(ch))
17
+ depth++;
18
+ else if ('}])>'.includes(ch))
19
+ depth--;
20
+ if (ch === ',' && depth === 0) {
21
+ parts.push(cur);
22
+ cur = '';
23
+ }
24
+ else
25
+ cur += ch;
26
+ }
27
+ if (cur.trim())
28
+ parts.push(cur);
29
+ return parts
30
+ .map((p) => p.trim())
31
+ .filter(Boolean)
32
+ .map((p) => {
33
+ const m = /^(\w+)(\??)\s*:\s*([\s\S]+)$/.exec(p);
34
+ return m ? { name: m[1], optional: m[2] === '?', type: m[3].trim() } : { name: p, optional: false, type: 'unknown' };
35
+ });
36
+ }
37
+ /** useUser for getUser, useUsers for listUsers? No: only a leading "get" is dropped (getUser → useUser, listUsers → useListUsers). */
38
+ export function hookName(fn) {
39
+ const rest = /^get[A-Z]/.test(fn) ? fn.slice(3) : fn;
40
+ return `use${pascal(rest)}`;
41
+ }
42
+ export function renderQueryHook(spec, fn, params, resourceKey, s) {
43
+ const name = hookName(fn);
44
+ const { q, semi } = s;
45
+ const i1 = s.indent;
46
+ const i2 = s.indent.repeat(2);
47
+ const sig = params.map((p) => `${p.name}${p.optional ? '?' : ''}: ${p.type}`).join(', ');
48
+ const names = params.map((p) => p.name);
49
+ if (spec.method === 'GET') {
50
+ const key = [jsString(resourceKey, q), ...names].join(', ');
51
+ return {
52
+ name,
53
+ uses: ['useQuery'],
54
+ code: [
55
+ `/** ${spec.method} ${commentText(spec.path)} */`,
56
+ `export function ${name}(${sig}) {`,
57
+ `${i1}return useQuery({`,
58
+ `${i2}queryKey: [${key}],`,
59
+ `${i2}queryFn: () => ${fn}(${names.join(', ')}),`,
60
+ `${i1}})${semi}`,
61
+ '}',
62
+ ].join('\n'),
63
+ };
64
+ }
65
+ let vars = '()';
66
+ let call = `${fn}()`;
67
+ if (params.length === 1) {
68
+ vars = `(${params[0].name}: ${params[0].type}${params[0].optional ? ' | undefined' : ''})`;
69
+ call = `${fn}(${params[0].name})`;
70
+ }
71
+ else if (params.length > 1) {
72
+ vars = `({ ${names.join(', ')} }: { ${params.map((p) => `${p.name}${p.optional ? '?' : ''}: ${p.type}`).join('; ')} })`;
73
+ call = `${fn}(${names.join(', ')})`;
74
+ }
75
+ return {
76
+ name,
77
+ uses: ['useMutation', 'useQueryClient'],
78
+ code: [
79
+ `/** ${spec.method} ${commentText(spec.path)} */`,
80
+ `export function ${name}() {`,
81
+ `${i1}const queryClient = useQueryClient()${semi}`,
82
+ `${i1}return useMutation({`,
83
+ `${i2}mutationFn: ${vars} => ${call},`,
84
+ `${i2}onSuccess: () => queryClient.invalidateQueries({ queryKey: [${jsString(resourceKey, q)}] }),`,
85
+ `${i1}})${semi}`,
86
+ '}',
87
+ ].join('\n'),
88
+ };
89
+ }
@@ -0,0 +1,150 @@
1
+ // React web `setup`: an API layer (and React Router, when there is none) for a
2
+ // Vite or Create React App project that has neither yet.
3
+ //
4
+ // fetch-helper src/api/client.ts with apiFetch<T>() and ApiError
5
+ // axios src/api/client.ts exporting an axios instance
6
+ // axios-react-query the axios instance plus TanStack Query (QueryClientProvider)
7
+ //
8
+ // The API URL lives in src/config/env.ts, read from VITE_API_URL (Vite) or
9
+ // REACT_APP_API_URL (Create React App). Without a router, React Router's
10
+ // createBrowserRouter is added in src/router.tsx with a Home page, and App
11
+ // renders it, so screen add works right away.
12
+ import { join } from 'node:path';
13
+ import { readJson, readText } from "../core/fsutil.js";
14
+ import { ToolError } from "../core/types.js";
15
+ import { axiosClient, fetchClient } from "./reactNativeSetup.js";
16
+ /** Versions known to work with the generated code (React 18/19). */
17
+ const PACKAGES = {
18
+ axios: '^1.7.7',
19
+ '@tanstack/react-query': '^5.59.0',
20
+ 'react-router-dom': '^6.28.0',
21
+ };
22
+ export function reactArchitectures(map) {
23
+ const c = map.conventions;
24
+ if (map.endpoints.length > 0 || (c.httpHelper && c.httpHelper !== 'fetch') || c.http === 'axios')
25
+ return [];
26
+ return [
27
+ { id: 'fetch-helper', label: 'Fetch helper', description: 'src/api/client.ts with apiFetch<T>() and ApiError; API URL in src/config/env.ts', recommended: true },
28
+ { id: 'axios', label: 'axios', description: 'An axios instance in src/api/client.ts; API functions call it', recommended: false },
29
+ { id: 'axios-react-query', label: 'axios + TanStack Query', description: 'The axios instance, plus a query hook for every endpoint and QueryClientProvider around the app', recommended: false },
30
+ ];
31
+ }
32
+ function envFile(cra, baseUrl, q, semi) {
33
+ const v = cra ? 'process.env.REACT_APP_API_URL' : 'import.meta.env.VITE_API_URL';
34
+ return [`// App settings, read from .env at build time (${cra ? 'REACT_APP_*' : 'VITE_*'} variables).`, `export const API_URL: string = ${v} ?? ${q}${baseUrl}${q}${semi}`, ''].join('\n');
35
+ }
36
+ function routerFile(q, semi) {
37
+ return [
38
+ `import { createBrowserRouter } from ${q}react-router-dom${q}${semi}`,
39
+ `import HomePage from ${q}./pages/HomePage${q}${semi}`,
40
+ '',
41
+ '/** Every page and its path. codz route add and screen add add entries here. */',
42
+ 'export const router = createBrowserRouter([',
43
+ ` { path: ${q}/${q}, element: <HomePage /> },`,
44
+ `])${semi}`,
45
+ '',
46
+ ].join('\n');
47
+ }
48
+ function homePage(semi) {
49
+ return ['export default function HomePage() {', ' return (', ' <main>', ' <h1>Home</h1>', ' </main>', ` )${semi}`, '}', ''].join('\n');
50
+ }
51
+ function appFile(nav, query, q, semi) {
52
+ const imports = [
53
+ ...(nav ? [`import { RouterProvider } from ${q}react-router-dom${q}${semi}`] : []),
54
+ ...(query ? [`import { QueryClient, QueryClientProvider } from ${q}@tanstack/react-query${q}${semi}`] : []),
55
+ ...(nav ? [`import { router } from ${q}./router${q}${semi}`] : []),
56
+ ];
57
+ let body = nav ? '<RouterProvider router={router} />' : '<main>\n <h1>Home</h1>\n </main>';
58
+ if (query)
59
+ body = `<QueryClientProvider client={queryClient}>\n ${body.replace(/\n/g, '\n ')}\n </QueryClientProvider>`;
60
+ return [...imports, '', ...(query ? [`const queryClient = new QueryClient()${semi}`, ''] : []), 'export default function App() {', ' return (', ` ${body}`, ` )${semi}`, '}', ''].join('\n');
61
+ }
62
+ export function planReactSetup(ctx, choice) {
63
+ const { root, map } = ctx;
64
+ if (!['fetch-helper', 'axios', 'axios-react-query'].includes(choice.architecture))
65
+ throw new ToolError('bad_args', `Unknown architecture "${choice.architecture}". Choose fetch-helper, axios or axios-react-query.`);
66
+ const c0 = map.conventions;
67
+ const cra = c0.bundler === 'cra';
68
+ const src = 'src';
69
+ const q = c0.quote === 'double' ? '"' : "'";
70
+ const semi = c0.semicolons === 'false' ? '' : ';';
71
+ const baseUrl = choice.baseUrl || 'https://api.example.com';
72
+ const files = new Map();
73
+ const rewrites = [];
74
+ const summary = [];
75
+ const warnings = [];
76
+ if (!choice.baseUrl)
77
+ warnings.push(`No base URL was given, so ${src}/config/env.ts uses https://api.example.com. Change it there or in .env before calling the API.`);
78
+ const read = (p) => readText(join(root, p));
79
+ const create = (path, content, what) => {
80
+ if (read(path) !== null)
81
+ 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.');
82
+ files.set(path, content);
83
+ summary.push(`Create ${path}: ${what}`);
84
+ };
85
+ const axios = choice.architecture !== 'fetch-helper';
86
+ const query = choice.architecture === 'axios-react-query';
87
+ const nav = c0.router === 'none' || !c0.router;
88
+ const envVar = cra ? 'REACT_APP_API_URL' : 'VITE_API_URL';
89
+ create(`${src}/config/env.ts`, envFile(cra, baseUrl, q, semi), `API_URL from ${envVar}`);
90
+ create(`${src}/api/client.ts`, axios ? axiosClient(q, semi) : fetchClient(q, semi), axios ? 'the axios instance (baseURL from API_URL)' : 'apiFetch<T>() and ApiError');
91
+ if (!axios)
92
+ create(`${src}/api/index.ts`, `export * from ${q}./client${q}${semi}\n`, 'exports every API file');
93
+ const env = read('.env.example');
94
+ if (!env || !env.includes(envVar)) {
95
+ files.set('.env.example', `${env ? env.replace(/\s*$/, '\n') : ''}${envVar}=${baseUrl}\n`);
96
+ summary.push(`${env ? 'Update' : 'Create'} .env.example: ${envVar}`);
97
+ }
98
+ if (!cra && read(`${src}/vite-env.d.ts`) === null) {
99
+ files.set(`${src}/vite-env.d.ts`, '/// <reference types="vite/client" />\n');
100
+ summary.push(`Create ${src}/vite-env.d.ts: types for import.meta.env`);
101
+ }
102
+ if (nav) {
103
+ create(`${src}/router.tsx`, routerFile(q, semi), 'React Router (createBrowserRouter) with the Home page');
104
+ create(`${src}/pages/HomePage.tsx`, homePage(semi), 'the first page');
105
+ }
106
+ if (nav || query) {
107
+ const app = [`${src}/App.tsx`, `${src}/App.jsx`, `${src}/App.js`].find((f) => read(f) !== null) ?? `${src}/App.tsx`;
108
+ files.set(app, appFile(nav, query, q, semi));
109
+ rewrites.push(app); // replaced on purpose; the previous App is in codz undo
110
+ summary.push(`${read(app) === null ? 'Create' : 'Replace'} ${app}: ${[nav && 'RouterProvider with the router', query && 'QueryClientProvider'].filter(Boolean).join(' and ')}${read(app) !== null ? ' (the previous App is in codz undo)' : ''}`);
111
+ }
112
+ // Dependencies.
113
+ const pkgText = read('package.json');
114
+ let dependenciesChanged = false;
115
+ if (pkgText !== null) {
116
+ const pkg = readJson(join(root, 'package.json')) ?? {};
117
+ const want = [...(axios ? ['axios'] : []), ...(query ? ['@tanstack/react-query'] : []), ...(nav ? ['react-router-dom'] : [])];
118
+ const missing = want.filter((p) => !pkg.dependencies?.[p] && !pkg.devDependencies?.[p]);
119
+ if (missing.length) {
120
+ const parsed = JSON.parse(pkgText);
121
+ parsed.dependencies = { ...parsed.dependencies };
122
+ for (const p of missing)
123
+ parsed.dependencies[p] = PACKAGES[p];
124
+ parsed.dependencies = Object.fromEntries(Object.entries(parsed.dependencies).sort(([a], [b]) => a.localeCompare(b)));
125
+ files.set('package.json', JSON.stringify(parsed, null, 2) + '\n');
126
+ summary.push(`Update package.json: add ${missing.join(', ')}`);
127
+ dependenciesChanged = true;
128
+ }
129
+ }
130
+ const conventions = {
131
+ ...c0,
132
+ http: axios ? 'axios' : 'fetch',
133
+ httpHelper: axios ? 'fetch' : 'apiFetch',
134
+ httpHelperFile: axios ? '' : `${src}/api/client.ts`,
135
+ apiFolder: `${src}/api`,
136
+ modelsFolder: `${src}/types`,
137
+ modelsLayout: 'per-file',
138
+ configFile: `${src}/config/env.ts`,
139
+ pagesFolder: `${src}/pages`,
140
+ ...(axios ? { axiosInstance: 'api', axiosInstanceFile: `${src}/api/client.ts` } : {}),
141
+ ...(query ? { dataFetching: 'react-query', hooksFolder: `${src}/hooks` } : {}),
142
+ ...(nav ? { router: 'react-router', routerFile: `${src}/router.tsx`, routerStyle: 'data' } : {}),
143
+ };
144
+ if (!axios)
145
+ for (const k of ['axiosInstance', 'axiosInstanceFile'])
146
+ delete conventions[k];
147
+ delete conventions.baseUrlEnv;
148
+ summary.push(`Save conventions: API functions in ${src}/api, interfaces in ${src}/types, pages in ${src}/pages`);
149
+ return { files, summary, warnings, generated: [], conventions, dependenciesChanged, rewrites };
150
+ }