proteum 1.0.2 → 2.0.0-1

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 (185) hide show
  1. package/AGENTS.md +101 -0
  2. package/agents/codex/AGENTS.md +95 -0
  3. package/agents/codex/CODING_STYLE.md +71 -0
  4. package/agents/codex/agents.md.zip +0 -0
  5. package/agents/codex/client/AGENTS.md +102 -0
  6. package/agents/codex/client/pages/AGENTS.md +35 -0
  7. package/agents/codex/server/routes/AGENTS.md +12 -0
  8. package/agents/codex/server/services/AGENTS.md +137 -0
  9. package/agents/codex/tests/AGENTS.md +8 -0
  10. package/cli/app/config.ts +13 -11
  11. package/cli/app/index.ts +74 -82
  12. package/cli/bin.js +1 -1
  13. package/cli/commands/build.ts +51 -14
  14. package/cli/commands/check.ts +19 -0
  15. package/cli/commands/deploy/app.ts +4 -8
  16. package/cli/commands/deploy/web.ts +16 -20
  17. package/cli/commands/dev.ts +189 -64
  18. package/cli/commands/devEvents.ts +106 -0
  19. package/cli/commands/init.ts +63 -57
  20. package/cli/commands/lint.ts +21 -0
  21. package/cli/commands/refresh.ts +18 -0
  22. package/cli/commands/typecheck.ts +18 -0
  23. package/cli/compiler/client/identite.ts +80 -53
  24. package/cli/compiler/client/index.ts +139 -213
  25. package/cli/compiler/common/bundleAnalysis.ts +94 -0
  26. package/cli/compiler/common/clientManifest.ts +67 -0
  27. package/cli/compiler/common/controllers.ts +288 -0
  28. package/cli/compiler/common/files/autres.ts +7 -18
  29. package/cli/compiler/common/files/images.ts +40 -37
  30. package/cli/compiler/common/files/style.ts +11 -22
  31. package/cli/compiler/common/generatedRouteModules.ts +368 -0
  32. package/cli/compiler/common/index.ts +31 -65
  33. package/cli/compiler/common/loaders/forbid-ssr-import.js +13 -0
  34. package/cli/compiler/common/rspackAliases.ts +13 -0
  35. package/cli/compiler/common/scripts.ts +37 -0
  36. package/cli/compiler/index.ts +781 -230
  37. package/cli/compiler/server/index.ts +59 -75
  38. package/cli/compiler/writeIfChanged.ts +21 -0
  39. package/cli/index.ts +71 -72
  40. package/cli/paths.ts +51 -57
  41. package/cli/print.ts +17 -11
  42. package/cli/tsconfig.json +5 -4
  43. package/cli/utils/agents.ts +100 -0
  44. package/cli/utils/check.ts +71 -0
  45. package/cli/utils/index.ts +1 -3
  46. package/cli/utils/keyboard.ts +8 -25
  47. package/cli/utils/runProcess.ts +30 -0
  48. package/client/app/component.tsx +29 -29
  49. package/client/app/index.ts +36 -57
  50. package/client/app/service.ts +7 -12
  51. package/client/app.tsconfig.json +2 -2
  52. package/client/components/Dialog/Manager.ssr.tsx +40 -0
  53. package/client/components/Dialog/Manager.tsx +119 -150
  54. package/client/components/Dialog/status.tsx +3 -3
  55. package/client/components/index.ts +1 -1
  56. package/client/components/types.d.ts +1 -3
  57. package/client/dev/hmr.ts +65 -0
  58. package/client/global.d.ts +2 -2
  59. package/client/hooks.ts +6 -9
  60. package/client/index.ts +2 -1
  61. package/client/islands/index.ts +7 -0
  62. package/client/islands/useDeferredModule.ts +199 -0
  63. package/client/pages/_layout/index.tsx +4 -12
  64. package/client/pages/useHeader.tsx +14 -21
  65. package/client/router.ts +27 -0
  66. package/client/services/router/components/Link.tsx +34 -27
  67. package/client/services/router/components/Page.tsx +6 -14
  68. package/client/services/router/components/router.ssr.tsx +36 -0
  69. package/client/services/router/components/router.tsx +63 -83
  70. package/client/services/router/index.tsx +185 -220
  71. package/client/services/router/request/api.ts +97 -119
  72. package/client/services/router/request/history.ts +2 -2
  73. package/client/services/router/request/index.ts +13 -12
  74. package/client/services/router/request/multipart.ts +72 -62
  75. package/client/services/router/response/index.tsx +68 -61
  76. package/client/services/router/response/page.ts +28 -32
  77. package/client/utils/dom.ts +17 -33
  78. package/common/app/index.ts +3 -3
  79. package/common/data/chaines/index.ts +22 -23
  80. package/common/data/dates.ts +35 -70
  81. package/common/data/markdown.ts +42 -39
  82. package/common/dev/serverHotReload.ts +26 -0
  83. package/common/errors/index.tsx +110 -142
  84. package/common/router/contracts.ts +29 -0
  85. package/common/router/index.ts +89 -108
  86. package/common/router/layouts.ts +34 -47
  87. package/common/router/pageSetup.ts +50 -0
  88. package/common/router/register.ts +53 -24
  89. package/common/router/request/api.ts +30 -36
  90. package/common/router/request/index.ts +2 -8
  91. package/common/router/response/index.ts +8 -15
  92. package/common/router/response/page.ts +70 -58
  93. package/common/utils.ts +1 -1
  94. package/doc/TODO.md +1 -1
  95. package/eslint.js +62 -0
  96. package/package.json +14 -49
  97. package/prettier.config.cjs +9 -0
  98. package/scripts/cleanup-generated-controllers.ts +62 -0
  99. package/scripts/fix-reference-app-typing.ts +490 -0
  100. package/scripts/refactor-client-app-imports.ts +244 -0
  101. package/scripts/refactor-client-pages.ts +587 -0
  102. package/scripts/refactor-server-controllers.ts +470 -0
  103. package/scripts/refactor-server-runtime-aliases.ts +360 -0
  104. package/scripts/restore-client-app-import-files.ts +41 -0
  105. package/scripts/restore-files-from-git-head.ts +20 -0
  106. package/scripts/update-codex-agents.ts +35 -0
  107. package/server/app/commands.ts +35 -64
  108. package/server/app/container/config.ts +48 -59
  109. package/server/app/container/console/index.ts +202 -248
  110. package/server/app/container/index.ts +33 -71
  111. package/server/app/controller/index.ts +61 -0
  112. package/server/app/index.ts +39 -105
  113. package/server/app/service/container.ts +41 -42
  114. package/server/app/service/index.ts +120 -147
  115. package/server/context.ts +1 -1
  116. package/server/index.ts +25 -1
  117. package/server/services/auth/index.ts +75 -115
  118. package/server/services/auth/router/index.ts +31 -32
  119. package/server/services/auth/router/request.ts +14 -16
  120. package/server/services/cron/CronTask.ts +13 -26
  121. package/server/services/cron/index.ts +14 -36
  122. package/server/services/disks/driver.ts +40 -58
  123. package/server/services/disks/drivers/local/index.ts +79 -90
  124. package/server/services/disks/drivers/s3/index.ts +116 -163
  125. package/server/services/disks/index.ts +23 -38
  126. package/server/services/email/index.ts +45 -104
  127. package/server/services/email/utils.ts +14 -27
  128. package/server/services/fetch/index.ts +53 -85
  129. package/server/services/prisma/Facet.ts +39 -91
  130. package/server/services/prisma/index.ts +74 -110
  131. package/server/services/router/generatedRuntime.ts +29 -0
  132. package/server/services/router/http/index.ts +78 -73
  133. package/server/services/router/http/multipart.ts +19 -42
  134. package/server/services/router/index.ts +378 -365
  135. package/server/services/router/request/api.ts +26 -25
  136. package/server/services/router/request/index.ts +44 -51
  137. package/server/services/router/request/service.ts +7 -11
  138. package/server/services/router/request/validation/zod.ts +111 -148
  139. package/server/services/router/response/index.ts +110 -125
  140. package/server/services/router/response/mask/Filter.ts +31 -72
  141. package/server/services/router/response/mask/index.ts +8 -15
  142. package/server/services/router/response/mask/selecteurs.ts +11 -25
  143. package/server/services/router/response/page/clientManifest.ts +25 -0
  144. package/server/services/router/response/page/document.tsx +199 -127
  145. package/server/services/router/response/page/index.tsx +89 -94
  146. package/server/services/router/service.ts +13 -15
  147. package/server/services/schema/index.ts +17 -26
  148. package/server/services/schema/request.ts +19 -33
  149. package/server/services/schema/router/index.ts +8 -11
  150. package/server/services/security/encrypt/aes/index.ts +15 -35
  151. package/server/utils/slug.ts +29 -35
  152. package/skills/clean-project-code/SKILL.md +63 -0
  153. package/skills/clean-project-code/agents/openai.yaml +4 -0
  154. package/tsconfig.common.json +4 -3
  155. package/tsconfig.json +4 -1
  156. package/types/aliases.d.ts +17 -21
  157. package/types/controller-input.test.ts +48 -0
  158. package/types/express-extra.d.ts +6 -0
  159. package/types/global/constants.d.ts +13 -0
  160. package/types/global/express-extra.d.ts +6 -0
  161. package/types/global/modules.d.ts +13 -16
  162. package/types/global/utils.d.ts +17 -49
  163. package/types/global/vendors.d.ts +62 -0
  164. package/types/icons.d.ts +65 -1
  165. package/types/uuid.d.ts +3 -0
  166. package/types/vendors.d.ts +62 -0
  167. package/cli/compiler/common/babel/index.ts +0 -170
  168. package/cli/compiler/common/babel/plugins/index.ts +0 -0
  169. package/cli/compiler/common/babel/plugins/services.ts +0 -586
  170. package/cli/compiler/common/babel/routes/imports.ts +0 -127
  171. package/cli/compiler/common/babel/routes/routes.ts +0 -1130
  172. package/client/services/captcha/index.ts +0 -67
  173. package/client/services/socket/index.ts +0 -147
  174. package/common/data/rte/nodes.ts +0 -83
  175. package/common/data/stats.ts +0 -90
  176. package/common/utils/rte.ts +0 -183
  177. package/server/services/auth/old.ts +0 -277
  178. package/server/services/cache/commands.ts +0 -41
  179. package/server/services/cache/index.ts +0 -297
  180. package/server/services/cache/service.json +0 -6
  181. package/server/services/socket/index.ts +0 -162
  182. package/server/services/socket/scope.ts +0 -226
  183. package/server/services/socket/service.json +0 -6
  184. package/server/services_old/SocketClient.ts +0 -92
  185. package/server/services_old/Token.old.ts +0 -97
@@ -0,0 +1,470 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { parse } from '@babel/parser';
9
+ import traverse, { NodePath } from '@babel/traverse';
10
+ import generate from '@babel/generator';
11
+ import * as types from '@babel/types';
12
+
13
+ /*----------------------------------
14
+ - TYPES
15
+ ----------------------------------*/
16
+
17
+ type TRouteMethod = { methodName: string; routePath: string; schemaSource?: string; schemaImports: string[] };
18
+
19
+ type TServiceRoot = { alias: string; id: string; dir: string };
20
+
21
+ type TImportBinding = { source: '@app' | '@models' | '@request'; imported: string };
22
+
23
+ /*----------------------------------
24
+ - HELPERS
25
+ ----------------------------------*/
26
+
27
+ const lowerFirst = (value: string) => (value.length ? value[0].toLowerCase() + value.substring(1) : value);
28
+
29
+ const findFiles = (dir: string, predicate: (filepath: string) => boolean): string[] => {
30
+ if (!fs.existsSync(dir)) return [];
31
+
32
+ const files: string[] = [];
33
+
34
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
35
+ const filepath = path.join(dir, dirent.name);
36
+
37
+ if (dirent.isDirectory()) {
38
+ files.push(...findFiles(filepath, predicate));
39
+ continue;
40
+ }
41
+
42
+ if (dirent.isFile() && predicate(filepath)) files.push(filepath);
43
+ }
44
+
45
+ return files;
46
+ };
47
+
48
+ const findNearestServiceRoot = (serviceRoots: TServiceRoot[], filepath: string) => {
49
+ const normalizedFilepath = filepath.replace(/\\/g, '/');
50
+
51
+ return serviceRoots
52
+ .filter((serviceRoot) => normalizedFilepath.startsWith(serviceRoot.dir.replace(/\\/g, '/') + '/'))
53
+ .sort((a, b) => b.dir.length - a.dir.length)[0];
54
+ };
55
+
56
+ const getRelativeServiceSegments = (serviceRootDir: string, filepath: string) => {
57
+ const relativePath = path.relative(serviceRootDir, filepath).replace(/\\/g, '/');
58
+ const segments = relativePath.split('/');
59
+ const filename = segments.pop() as string;
60
+ const basename = filename.replace(/\.(tsx?|jsx?)$/, '');
61
+
62
+ if (basename !== 'index') segments.push(basename);
63
+
64
+ return segments.filter(Boolean);
65
+ };
66
+
67
+ const getControllerSegments = (relativePath: string) => {
68
+ const segments = relativePath
69
+ .replace(/\.controller\.ts$/, '')
70
+ .split('/')
71
+ .filter(Boolean);
72
+
73
+ if (segments.length > 1 && segments[segments.length - 1] === segments[segments.length - 2]) {
74
+ segments.pop();
75
+ }
76
+
77
+ return segments;
78
+ };
79
+
80
+ const getControllerBasePath = (serviceRoot: TServiceRoot, controllerFilepath: string) => {
81
+ const segments = getControllerSegments(path.relative(serviceRoot.dir, controllerFilepath).replace(/\\/g, '/'));
82
+
83
+ if (segments[0]?.toLowerCase() === serviceRoot.alias.toLowerCase()) {
84
+ segments.shift();
85
+ }
86
+
87
+ return [serviceRoot.alias, ...segments].filter(Boolean).join('/');
88
+ };
89
+
90
+ const findServiceAliases = (repoRoot: string) => {
91
+ const serviceAliasById = new Map<string, string>();
92
+ const configFiles = findFiles(path.join(repoRoot, 'server', 'config'), (filepath) => filepath.endsWith('.ts'));
93
+
94
+ for (const configFile of configFiles) {
95
+ const content = fs.readFileSync(configFile, 'utf8');
96
+ const regex = /app\.setup\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
97
+
98
+ for (let match = regex.exec(content); match; match = regex.exec(content))
99
+ serviceAliasById.set(match[2], match[1]);
100
+ }
101
+
102
+ return serviceAliasById;
103
+ };
104
+
105
+ const findServiceRoots = (repoRoot: string) => {
106
+ const aliasById = findServiceAliases(repoRoot);
107
+ const serviceJsonFiles = findFiles(
108
+ path.join(repoRoot, 'server', 'services'),
109
+ (filepath) => path.basename(filepath) === 'service.json',
110
+ );
111
+
112
+ return serviceJsonFiles
113
+ .map<TServiceRoot | null>((serviceJsonFile) => {
114
+ const metas = fs.readJsonSync(serviceJsonFile) as { id?: string };
115
+ if (!metas.id) return null;
116
+
117
+ const alias = aliasById.get(metas.id);
118
+ if (!alias) return null;
119
+
120
+ return { alias, id: metas.id, dir: path.dirname(serviceJsonFile) };
121
+ })
122
+ .filter((serviceRoot): serviceRoot is TServiceRoot => !!serviceRoot);
123
+ };
124
+
125
+ const getRouteDecoratorMeta = (decorator: types.Decorator) => {
126
+ if (decorator.expression.type !== 'CallExpression') return null;
127
+
128
+ const callee = decorator.expression.callee;
129
+ if (callee.type !== 'Identifier' || callee.name !== 'Route') return null;
130
+
131
+ const [firstArg, secondArg] = decorator.expression.arguments;
132
+ let routePath: string | undefined;
133
+ let schemaExpression: types.Expression | undefined;
134
+
135
+ if (!firstArg) return null;
136
+
137
+ if (types.isStringLiteral(firstArg)) {
138
+ routePath = firstArg.value;
139
+ if (secondArg && types.isExpression(secondArg)) schemaExpression = secondArg;
140
+ } else if (types.isObjectExpression(firstArg)) {
141
+ for (const property of firstArg.properties) {
142
+ if (property.type !== 'ObjectProperty' || property.key.type !== 'Identifier') continue;
143
+
144
+ if (property.key.name === 'path' && property.value.type === 'StringLiteral')
145
+ routePath = property.value.value;
146
+ else if (property.key.name === 'schema' && types.isExpression(property.value))
147
+ schemaExpression = property.value;
148
+ }
149
+ }
150
+
151
+ if (!routePath) return null;
152
+
153
+ return { routePath, schemaExpression };
154
+ };
155
+
156
+ const ensureNamedExport = (programPath: NodePath<types.Program>, identifierName: string) => {
157
+ let alreadyExported = false;
158
+
159
+ for (const statement of programPath.node.body) {
160
+ if (
161
+ statement.type === 'ExportNamedDeclaration' &&
162
+ statement.specifiers.some(
163
+ (specifier) =>
164
+ specifier.type === 'ExportSpecifier' &&
165
+ specifier.exported.type === 'Identifier' &&
166
+ specifier.exported.name === identifierName,
167
+ )
168
+ ) {
169
+ alreadyExported = true;
170
+ break;
171
+ }
172
+ }
173
+
174
+ if (alreadyExported) return;
175
+
176
+ const binding = programPath.scope.getBinding(identifierName);
177
+ if (!binding) return;
178
+
179
+ const declarationPath = binding.path.parentPath;
180
+ if (!declarationPath) return;
181
+
182
+ if (declarationPath.isExportNamedDeclaration()) return;
183
+
184
+ if (
185
+ declarationPath.isVariableDeclaration() ||
186
+ declarationPath.isFunctionDeclaration() ||
187
+ declarationPath.isClassDeclaration()
188
+ ) {
189
+ declarationPath.replaceWith(types.exportNamedDeclaration(declarationPath.node, []));
190
+ return;
191
+ }
192
+
193
+ programPath.pushContainer(
194
+ 'body',
195
+ types.exportNamedDeclaration(undefined, [
196
+ types.exportSpecifier(types.identifier(identifierName), types.identifier(identifierName)),
197
+ ]),
198
+ );
199
+ };
200
+
201
+ const buildMemberExpression = (...segments: string[]) => {
202
+ let expression: types.Expression = segments[0] === 'this' ? types.thisExpression() : types.identifier(segments[0]);
203
+
204
+ for (const segment of segments.slice(1)) expression = types.memberExpression(expression, types.identifier(segment));
205
+
206
+ return expression;
207
+ };
208
+
209
+ const replaceReferencedIdentifier = (identifierPath: NodePath<types.Identifier>, binding: TImportBinding) => {
210
+ if (!identifierPath.isReferencedIdentifier()) return;
211
+
212
+ if (binding.source === '@app') {
213
+ identifierPath.replaceWith(
214
+ types.memberExpression(
215
+ types.memberExpression(types.thisExpression(), types.identifier('services')),
216
+ types.identifier(binding.imported),
217
+ ),
218
+ );
219
+ return;
220
+ }
221
+
222
+ if (binding.source === '@models') {
223
+ identifierPath.replaceWith(
224
+ types.memberExpression(
225
+ types.memberExpression(types.thisExpression(), types.identifier('models')),
226
+ types.identifier(lowerFirst(binding.imported)),
227
+ ),
228
+ );
229
+ return;
230
+ }
231
+
232
+ const requestPathByImport: Record<string, string[]> = {
233
+ auth: ['this', 'request', 'auth'],
234
+ request: ['this', 'request', 'request'],
235
+ response: ['this', 'request', 'response'],
236
+ user: ['this', 'request', 'user'],
237
+ context: ['this', 'request', 'context'],
238
+ };
239
+
240
+ const memberSegments = requestPathByImport[binding.imported];
241
+ if (!memberSegments) return;
242
+
243
+ identifierPath.replaceWith(buildMemberExpression(...memberSegments));
244
+ };
245
+
246
+ const getControllerClassName = (serviceClassName: string, filepath: string) => {
247
+ const rawBasename = path.basename(filepath, path.extname(filepath));
248
+ const baseName =
249
+ serviceClassName || (rawBasename === 'index' ? path.basename(path.dirname(filepath)) : rawBasename);
250
+ return baseName.endsWith('Controller') ? baseName : `${baseName}Controller`;
251
+ };
252
+
253
+ /*----------------------------------
254
+ - TRANSFORM
255
+ ----------------------------------*/
256
+
257
+ const migrateServiceFile = (filepath: string, serviceRoots: TServiceRoot[]) => {
258
+ const code = fs.readFileSync(filepath, 'utf8');
259
+ if (!/@Route\(|from ['"]@app['"]|from ['"]@models['"]|from ['"]@request['"]/.test(code)) return false;
260
+
261
+ const ast = parse(code, {
262
+ sourceType: 'module',
263
+ plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties'],
264
+ });
265
+
266
+ const importBindings = new Map<string, TImportBinding>();
267
+ const schemaExports = new Set<string>();
268
+ const routeMethods: TRouteMethod[] = [];
269
+ let fileChanged = false;
270
+
271
+ const serviceRoot = findNearestServiceRoot(serviceRoots, filepath);
272
+
273
+ let serviceClassName = '';
274
+
275
+ traverse(ast, {
276
+ ImportDeclaration(path) {
277
+ const source = path.node.source.value;
278
+
279
+ if (source === '@server/app/service') {
280
+ const beforeLength = path.node.specifiers.length;
281
+ path.node.specifiers = path.node.specifiers.filter(
282
+ (specifier) =>
283
+ !(
284
+ specifier.type === 'ImportSpecifier' &&
285
+ specifier.imported.type === 'Identifier' &&
286
+ specifier.imported.name === 'Route'
287
+ ),
288
+ );
289
+
290
+ if (path.node.specifiers.length !== beforeLength) fileChanged = true;
291
+
292
+ if (!path.node.specifiers.length) path.remove();
293
+
294
+ return;
295
+ }
296
+
297
+ if (source !== '@app' && source !== '@models' && source !== '@request') return;
298
+
299
+ for (const specifier of path.node.specifiers) {
300
+ if (specifier.type !== 'ImportSpecifier' || specifier.imported.type !== 'Identifier') continue;
301
+
302
+ importBindings.set(specifier.local.name, {
303
+ source: source as TImportBinding['source'],
304
+ imported: specifier.imported.name,
305
+ });
306
+ }
307
+
308
+ path.remove();
309
+ fileChanged = true;
310
+ },
311
+
312
+ ExportDefaultDeclaration(path) {
313
+ const declaration = path.node.declaration;
314
+
315
+ if (declaration.type === 'ClassDeclaration' && declaration.id?.name) serviceClassName = declaration.id.name;
316
+ },
317
+
318
+ ClassMethod(path) {
319
+ if (path.node.key.type !== 'Identifier' || !path.node.decorators?.length) return;
320
+
321
+ const nextDecorators: types.Decorator[] = [];
322
+
323
+ for (const decorator of path.node.decorators) {
324
+ const routeMeta = getRouteDecoratorMeta(decorator);
325
+ if (!routeMeta) {
326
+ nextDecorators.push(decorator);
327
+ continue;
328
+ }
329
+
330
+ const schemaImports: string[] = [];
331
+ const methodName = path.node.key.name;
332
+ const schemaExpression = routeMeta.schemaExpression;
333
+ const schemaSource = schemaExpression ? generate(schemaExpression).code : undefined;
334
+
335
+ if (schemaExpression?.type === 'Identifier') {
336
+ schemaExports.add(schemaExpression.name);
337
+ schemaImports.push(schemaExpression.name);
338
+ }
339
+
340
+ routeMethods.push({ methodName, routePath: routeMeta.routePath, schemaSource, schemaImports });
341
+
342
+ fileChanged = true;
343
+ }
344
+
345
+ path.node.decorators = nextDecorators.length ? nextDecorators : undefined;
346
+
347
+ path.traverse({
348
+ Identifier(identifierPath) {
349
+ const binding = importBindings.get(identifierPath.node.name);
350
+ if (!binding) return;
351
+
352
+ if (identifierPath.scope.getBinding(identifierPath.node.name)?.path.isImportSpecifier() !== true)
353
+ return;
354
+
355
+ replaceReferencedIdentifier(identifierPath, binding);
356
+ fileChanged = true;
357
+ },
358
+ });
359
+ },
360
+ });
361
+
362
+ if (!fileChanged) return false;
363
+
364
+ traverse(ast, {
365
+ Program(programPath) {
366
+ for (const schemaExport of schemaExports) ensureNamedExport(programPath, schemaExport);
367
+ },
368
+ });
369
+
370
+ fs.writeFileSync(filepath, generate(ast, { decoratorsBeforeExport: true }, code).code);
371
+
372
+ if (!routeMethods.length) return true;
373
+
374
+ if (!serviceRoot) throw new Error(`Unable to find the parent service root for ${filepath}`);
375
+
376
+ const serviceSegments = getRelativeServiceSegments(serviceRoot.dir, filepath);
377
+ const serviceAccessPath = [serviceRoot.alias, ...serviceSegments].join('.');
378
+
379
+ const controllerBasename =
380
+ path.basename(filepath, path.extname(filepath)) === 'index'
381
+ ? `${path.basename(path.dirname(filepath))}.controller.ts`
382
+ : `${path.basename(filepath, path.extname(filepath))}.controller.ts`;
383
+ const controllerFilepath = path.join(path.dirname(filepath), controllerBasename);
384
+ const relativeServiceImportPath = './' + path.basename(filepath, path.extname(filepath));
385
+ const schemaImports = [...new Set(routeMethods.flatMap((routeMethod) => routeMethod.schemaImports))];
386
+ const controllerClassName = getControllerClassName(serviceClassName, filepath);
387
+ const needsSchemaHelperImport = routeMethods.some((routeMethod) => routeMethod.schemaSource?.includes('schema.'));
388
+ const defaultControllerPath = getControllerBasePath(serviceRoot, controllerFilepath);
389
+ const routeBasePaths = new Set<string>();
390
+
391
+ for (const routeMethod of routeMethods) {
392
+ const routeSegments = routeMethod.routePath.split('/').filter(Boolean);
393
+ const routeMethodName = routeSegments.pop();
394
+
395
+ if (routeMethodName !== routeMethod.methodName) {
396
+ throw new Error(
397
+ `Unable to migrate ${filepath}#${routeMethod.methodName}: route path ${JSON.stringify(routeMethod.routePath)} renames the method. ` +
398
+ 'Rename the method or split the controller manually.',
399
+ );
400
+ }
401
+
402
+ routeBasePaths.add(routeSegments.join('/'));
403
+ }
404
+
405
+ if (routeBasePaths.size > 1) {
406
+ throw new Error(
407
+ `Unable to migrate ${filepath}: methods use multiple route bases (${[...routeBasePaths].join(', ')}). ` +
408
+ 'Split the service into multiple controllers before migration.',
409
+ );
410
+ }
411
+
412
+ const controllerPath = [...routeBasePaths][0] || '';
413
+ const controllerPathExport =
414
+ controllerPath && controllerPath !== defaultControllerPath
415
+ ? `export const controllerPath = ${JSON.stringify(controllerPath)};\n\n`
416
+ : '';
417
+
418
+ const methodBlocks = routeMethods
419
+ .map((routeMethod) => {
420
+ const inputLine = routeMethod.schemaSource
421
+ ? ` const data = this.input(${routeMethod.schemaSource});\n`
422
+ : '';
423
+ const callArgs = routeMethod.schemaSource ? 'data' : '';
424
+
425
+ return ` public async ${routeMethod.methodName}() {
426
+ const { ${serviceRoot.alias} } = this.services;
427
+ ${inputLine}
428
+ return ${serviceAccessPath}.${routeMethod.methodName}(${callArgs});
429
+ }`;
430
+ })
431
+ .join('\n\n');
432
+
433
+ fs.writeFileSync(
434
+ controllerFilepath,
435
+ `import Controller${needsSchemaHelperImport ? ', { schema }' : ''} from '@server/app/controller';
436
+ ${schemaImports.length ? `import { ${schemaImports.join(', ')} } from ${JSON.stringify(relativeServiceImportPath)};\n` : ''}
437
+ ${controllerPathExport}
438
+ export default class ${controllerClassName} extends Controller {
439
+
440
+ ${methodBlocks}
441
+ }
442
+ `,
443
+ );
444
+
445
+ return true;
446
+ };
447
+
448
+ /*----------------------------------
449
+ - RUN
450
+ ----------------------------------*/
451
+
452
+ const repoRoots = process.argv.slice(2);
453
+ if (!repoRoots.length)
454
+ throw new Error('Usage: ts-node scripts/refactor-server-controllers.ts <repo-root> [repo-root...]');
455
+
456
+ for (const repoRoot of repoRoots) {
457
+ const serviceRoots = findServiceRoots(repoRoot);
458
+ const serviceFiles = findFiles(
459
+ path.join(repoRoot, 'server', 'services'),
460
+ (filepath) => /\.(tsx?|jsx?)$/.test(filepath) && !filepath.endsWith('.controller.ts'),
461
+ );
462
+
463
+ let migratedFiles = 0;
464
+
465
+ for (const serviceFile of serviceFiles) {
466
+ if (migrateServiceFile(serviceFile, serviceRoots)) migratedFiles++;
467
+ }
468
+
469
+ console.log(`[refactor-server-controllers] ${repoRoot}: migrated ${migratedFiles} service files`);
470
+ }