proteum 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +13 -1
  2. package/README.md +375 -0
  3. package/agents/framework/AGENTS.md +917 -0
  4. package/agents/project/AGENTS.md +138 -0
  5. package/agents/{codex → project}/CODING_STYLE.md +3 -2
  6. package/agents/project/client/AGENTS.md +108 -0
  7. package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
  8. package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
  9. package/agents/project/server/services/AGENTS.md +170 -0
  10. package/agents/{codex → project}/tests/AGENTS.md +1 -0
  11. package/cli/app/config.ts +3 -2
  12. package/cli/app/index.ts +6 -66
  13. package/cli/bin.js +7 -2
  14. package/cli/commands/build.ts +94 -27
  15. package/cli/commands/check.ts +15 -1
  16. package/cli/commands/dev.ts +288 -132
  17. package/cli/commands/doctor.ts +108 -0
  18. package/cli/commands/explain.ts +226 -0
  19. package/cli/commands/init.ts +76 -70
  20. package/cli/commands/lint.ts +18 -1
  21. package/cli/commands/refresh.ts +16 -6
  22. package/cli/commands/typecheck.ts +14 -1
  23. package/cli/compiler/artifacts/controllers.ts +150 -0
  24. package/cli/compiler/artifacts/discovery.ts +132 -0
  25. package/cli/compiler/artifacts/manifest.ts +267 -0
  26. package/cli/compiler/artifacts/routing.ts +315 -0
  27. package/cli/compiler/artifacts/services.ts +480 -0
  28. package/cli/compiler/artifacts/shared.ts +12 -0
  29. package/cli/compiler/client/identite.ts +2 -1
  30. package/cli/compiler/client/index.ts +13 -3
  31. package/cli/compiler/common/controllers.ts +23 -28
  32. package/cli/compiler/common/files/style.ts +3 -4
  33. package/cli/compiler/common/generatedRouteModules.ts +333 -19
  34. package/cli/compiler/common/proteumManifest.ts +133 -0
  35. package/cli/compiler/index.ts +33 -896
  36. package/cli/compiler/server/index.ts +21 -4
  37. package/cli/context.ts +71 -0
  38. package/cli/index.ts +39 -181
  39. package/cli/presentation/commands.ts +208 -0
  40. package/cli/presentation/compileReporter.ts +65 -0
  41. package/cli/presentation/devSession.ts +70 -0
  42. package/cli/presentation/help.ts +193 -0
  43. package/cli/presentation/ink.ts +69 -0
  44. package/cli/presentation/layout.ts +83 -0
  45. package/cli/runtime/argv.ts +49 -0
  46. package/cli/runtime/command.ts +25 -0
  47. package/cli/runtime/commands.ts +221 -0
  48. package/cli/runtime/importEsm.ts +7 -0
  49. package/cli/runtime/verbose.ts +15 -0
  50. package/cli/utils/agents.ts +5 -4
  51. package/cli/utils/keyboard.ts +12 -6
  52. package/client/app/index.ts +0 -6
  53. package/client/services/router/index.tsx +1 -1
  54. package/client/services/router/response/index.tsx +2 -2
  55. package/common/dev/serverHotReload.ts +12 -0
  56. package/common/router/index.ts +3 -2
  57. package/common/router/layouts.ts +1 -1
  58. package/common/router/pageSetup.ts +1 -0
  59. package/package.json +10 -8
  60. package/prettier/router-registration-plugin.cjs +52 -0
  61. package/prettier.config.cjs +1 -0
  62. package/scripts/cleanup-generated-controllers.ts +2 -2
  63. package/scripts/fix-reference-app-typing.ts +2 -2
  64. package/scripts/format-router-registrations.ts +119 -0
  65. package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
  66. package/scripts/refactor-server-controllers.ts +19 -18
  67. package/scripts/refactor-server-runtime-aliases.ts +1 -1
  68. package/server/app/commands.ts +309 -25
  69. package/server/app/container/config.ts +1 -1
  70. package/server/app/container/index.ts +2 -2
  71. package/server/app/controller/index.ts +13 -4
  72. package/server/app/index.ts +53 -37
  73. package/server/app/service/container.ts +26 -28
  74. package/server/app/service/index.ts +10 -20
  75. package/server/app.tsconfig.json +9 -2
  76. package/server/index.ts +32 -1
  77. package/server/services/auth/index.ts +234 -15
  78. package/server/services/auth/router/index.ts +39 -7
  79. package/server/services/auth/router/request.ts +40 -8
  80. package/server/services/disks/index.ts +1 -1
  81. package/server/services/prisma/Facet.ts +2 -2
  82. package/server/services/prisma/index.ts +22 -5
  83. package/server/services/prisma/mariadb.ts +47 -0
  84. package/server/services/router/http/index.ts +9 -1
  85. package/server/services/router/index.ts +10 -4
  86. package/server/services/router/response/index.ts +26 -6
  87. package/types/auth-check-rules.test.ts +51 -0
  88. package/types/controller-request-context.test.ts +55 -0
  89. package/types/service-config.test.ts +39 -0
  90. package/agents/codex/AGENTS.md +0 -95
  91. package/agents/codex/client/AGENTS.md +0 -102
  92. package/agents/codex/server/services/AGENTS.md +0 -137
  93. package/server/services/models.7z +0 -0
  94. /package/agents/{codex → project}/agents.md.zip +0 -0
@@ -0,0 +1,119 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createRequire } from 'module';
4
+ import { parse } from '@babel/parser';
5
+ import traverse, { NodePath } from '@babel/traverse';
6
+ import type * as types from '@babel/types';
7
+
8
+ const requireFromWorkspace = createRequire(path.join(process.cwd(), 'package.json'));
9
+ const prettier = requireFromWorkspace('prettier') as typeof import('prettier');
10
+ const prettierConfig = require(path.resolve(__dirname, '..', 'prettier.config.cjs'));
11
+
12
+ const ROUTER_REGISTRATION_PATTERN = /Router\.(page|error)\(/;
13
+ const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs']);
14
+
15
+ const findFiles = (dir: string): string[] => {
16
+ if (!fs.existsSync(dir)) return [];
17
+
18
+ const files: string[] = [];
19
+
20
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
21
+ const filepath = path.join(dir, dirent.name);
22
+
23
+ if (dirent.isDirectory()) {
24
+ files.push(...findFiles(filepath));
25
+ continue;
26
+ }
27
+
28
+ if (dirent.isFile() && SUPPORTED_EXTENSIONS.has(path.extname(filepath))) files.push(filepath);
29
+ }
30
+
31
+ return files;
32
+ };
33
+
34
+ const formatFile = async (filepath: string) => {
35
+ const source = fs.readFileSync(filepath, 'utf8');
36
+ if (!ROUTER_REGISTRATION_PATTERN.test(source)) return false;
37
+
38
+ const ast = parse(source, {
39
+ sourceType: 'module',
40
+ errorRecovery: true,
41
+ plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties'],
42
+ });
43
+
44
+ const replacements: Array<{ start: number; end: number; formatted: string }> = [];
45
+
46
+ traverse(ast, {
47
+ ExpressionStatement(routePath: NodePath<types.ExpressionStatement>) {
48
+ const expression = routePath.node.expression;
49
+ if (
50
+ expression.type !== 'CallExpression' ||
51
+ expression.callee.type !== 'MemberExpression' ||
52
+ expression.callee.object.type !== 'Identifier' ||
53
+ expression.callee.object.name !== 'Router' ||
54
+ expression.callee.property.type !== 'Identifier' ||
55
+ (expression.callee.property.name !== 'page' && expression.callee.property.name !== 'error')
56
+ )
57
+ return;
58
+
59
+ if (routePath.node.start == null || routePath.node.end == null) return;
60
+
61
+ replacements.push({
62
+ start: routePath.node.start,
63
+ end: routePath.node.end,
64
+ formatted: '',
65
+ });
66
+ },
67
+ });
68
+
69
+ if (!replacements.length) return false;
70
+
71
+ for (const replacement of replacements) {
72
+ const fragment = source.slice(replacement.start, replacement.end);
73
+ replacement.formatted = (await prettier.format(fragment, {
74
+ ...prettierConfig,
75
+ filepath,
76
+ })).trimEnd();
77
+ }
78
+
79
+ let nextSource = source;
80
+
81
+ for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
82
+ nextSource =
83
+ nextSource.slice(0, replacement.start) + replacement.formatted + nextSource.slice(replacement.end);
84
+ }
85
+
86
+ if (nextSource === source) return false;
87
+
88
+ fs.writeFileSync(filepath, nextSource);
89
+ return true;
90
+ };
91
+
92
+ const main = async () => {
93
+ const repoRoots = process.argv.slice(2);
94
+ if (!repoRoots.length) {
95
+ throw new Error('Usage: ts-node scripts/format-router-registrations.ts <repo-root> [repo-root...]');
96
+ }
97
+
98
+ let changedFiles = 0;
99
+
100
+ for (const repoRoot of repoRoots) {
101
+ const pagesRoot = path.join(repoRoot, 'client', 'pages');
102
+ const files = findFiles(pagesRoot);
103
+
104
+ for (const filepath of files) {
105
+ const changed = await formatFile(filepath);
106
+ if (!changed) continue;
107
+
108
+ changedFiles += 1;
109
+ console.log(`formatted ${filepath}`);
110
+ }
111
+ }
112
+
113
+ console.log(`formatted ${changedFiles} file(s)`);
114
+ };
115
+
116
+ main().catch((error) => {
117
+ console.error(error);
118
+ process.exit(1);
119
+ });
@@ -0,0 +1,423 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { parse } from '@babel/parser';
4
+ import traverse from '@babel/traverse';
5
+ import generate from '@babel/generator';
6
+ import * as t from '@babel/types';
7
+
8
+ type TControllerRouteMap = Map<string, string>;
9
+
10
+ const parserPlugins = [
11
+ 'typescript',
12
+ 'jsx',
13
+ 'classProperties',
14
+ 'classPrivateProperties',
15
+ 'classPrivateMethods',
16
+ 'decorators-legacy',
17
+ 'objectRestSpread',
18
+ 'optionalChaining',
19
+ 'nullishCoalescingOperator',
20
+ ] as const;
21
+
22
+ const parseModule = (filepath: string, code: string) =>
23
+ parse(code, {
24
+ sourceType: 'module',
25
+ sourceFilename: filepath,
26
+ plugins: [...parserPlugins],
27
+ });
28
+
29
+ const generateModule = (ast: ReturnType<typeof parse>) =>
30
+ generate(ast, {
31
+ retainLines: false,
32
+ decoratorsBeforeExport: true,
33
+ jsescOption: { minimal: true },
34
+ }).code + '\n';
35
+
36
+ const ensureRelativeImport = (value: string) =>
37
+ value.startsWith('.') ? value : value.startsWith('/') ? value : './' + value;
38
+
39
+ const stripTsExtension = (filepath: string) => filepath.replace(/\.(tsx?|jsx?)$/, '');
40
+
41
+ const resolveImportFile = (fromDir: string, source: string) => {
42
+ const candidates = [
43
+ path.resolve(fromDir, source),
44
+ path.resolve(fromDir, source + '.ts'),
45
+ path.resolve(fromDir, source + '.tsx'),
46
+ path.resolve(fromDir, source + '.js'),
47
+ path.resolve(fromDir, source + '.jsx'),
48
+ path.resolve(fromDir, source, 'index.ts'),
49
+ path.resolve(fromDir, source, 'index.tsx'),
50
+ path.resolve(fromDir, source, 'index.js'),
51
+ path.resolve(fromDir, source, 'index.jsx'),
52
+ ];
53
+
54
+ return candidates.find((candidate) => fs.existsSync(candidate));
55
+ };
56
+
57
+ const parseControllerRoutes = (repoRoot: string): TControllerRouteMap => {
58
+ const generatedPath = path.join(repoRoot, '.proteum', 'server', 'controllers.ts');
59
+ const code = fs.readFileSync(generatedPath, 'utf8');
60
+ const importRegex = /import\s+(Controller\d+)\s+from\s+"(@\/server\/services\/[^"]+)";/g;
61
+ const routeRegex =
62
+ /\{\s*path:\s*"([^"]+)",\s*Controller:\s*(Controller\d+),\s*method:\s*"([^"]+)"\s*,?\s*\}/g;
63
+
64
+ const controllerImportById = new Map<string, string>();
65
+ const routeBaseByImportPath = new Map<string, string>();
66
+
67
+ for (const match of code.matchAll(importRegex)) {
68
+ controllerImportById.set(match[1], match[2]);
69
+ }
70
+
71
+ for (const match of code.matchAll(routeRegex)) {
72
+ const httpPath = match[1];
73
+ const controllerId = match[2];
74
+ const importPath = controllerImportById.get(controllerId);
75
+
76
+ if (!importPath) continue;
77
+
78
+ const segments = httpPath.replace(/^\/api\//, '').split('/').filter(Boolean);
79
+ const routeBasePath = segments.slice(0, -1).join('/');
80
+
81
+ if (!routeBasePath) continue;
82
+ if (!routeBaseByImportPath.has(importPath)) routeBaseByImportPath.set(importPath, routeBasePath);
83
+ }
84
+
85
+ return routeBaseByImportPath;
86
+ };
87
+
88
+ const rewriteRelativeImports = (code: string, fromFilepath: string, toFilepath: string) => {
89
+ const ast = parseModule(fromFilepath, code);
90
+
91
+ traverse(ast, {
92
+ ImportDeclaration(importPath) {
93
+ const source = importPath.node.source.value;
94
+ if (typeof source !== 'string' || !source.startsWith('.')) return;
95
+
96
+ const resolved = resolveImportFile(path.dirname(fromFilepath), source);
97
+ if (!resolved) return;
98
+
99
+ const relative = stripTsExtension(path.relative(path.dirname(toFilepath), resolved)).replace(/\\/g, '/');
100
+ importPath.node.source = t.stringLiteral(ensureRelativeImport(relative));
101
+ },
102
+ });
103
+
104
+ return generateModule(ast);
105
+ };
106
+
107
+ const isThisMember = (node: t.Node | null | undefined, propertyName: string): node is t.MemberExpression =>
108
+ !!node &&
109
+ t.isMemberExpression(node) &&
110
+ t.isThisExpression(node.object) &&
111
+ !node.computed &&
112
+ t.isIdentifier(node.property, { name: propertyName });
113
+
114
+ const getMemberRoot = (node: t.Expression | t.PrivateName): t.Expression | t.PrivateName => {
115
+ let current: t.Expression | t.PrivateName = node;
116
+
117
+ while (t.isMemberExpression(current) || t.isOptionalMemberExpression(current)) {
118
+ current = current.object;
119
+ }
120
+
121
+ return current;
122
+ };
123
+
124
+ const collectPatternNames = (pattern: t.LVal, names: Set<string>) => {
125
+ if (t.isIdentifier(pattern)) {
126
+ names.add(pattern.name);
127
+ return;
128
+ }
129
+
130
+ if (t.isObjectPattern(pattern)) {
131
+ for (const property of pattern.properties) {
132
+ if (t.isRestElement(property)) {
133
+ collectPatternNames(property.argument, names);
134
+ continue;
135
+ }
136
+
137
+ if (t.isObjectProperty(property)) {
138
+ collectPatternNames(property.value as t.LVal, names);
139
+ }
140
+ }
141
+ return;
142
+ }
143
+
144
+ if (t.isArrayPattern(pattern)) {
145
+ for (const element of pattern.elements) {
146
+ if (!element) continue;
147
+ if (t.isRestElement(element)) {
148
+ collectPatternNames(element.argument, names);
149
+ continue;
150
+ }
151
+ collectPatternNames(element, names);
152
+ }
153
+ }
154
+ };
155
+
156
+ const isServiceLikeBinding = (init: t.Expression | null | undefined) => {
157
+ if (!init) return false;
158
+ if (isThisMember(init, 'services')) return true;
159
+ if (isThisMember(init, 'app')) return true;
160
+
161
+ if (t.isMemberExpression(init) || t.isOptionalMemberExpression(init)) {
162
+ const root = getMemberRoot(init);
163
+ return isThisMember(root, 'services') || isThisMember(root, 'app') || isThisMember(root, 'parent');
164
+ }
165
+
166
+ return false;
167
+ };
168
+
169
+ const hasExplicitRequestArgument = (args: Array<t.Expression | t.SpreadElement | t.ArgumentPlaceholder>) =>
170
+ args.some(
171
+ (arg) =>
172
+ t.isIdentifier(arg, { name: 'request' }) ||
173
+ (t.isMemberExpression(arg) &&
174
+ isThisMember(arg.object, 'request') &&
175
+ !arg.computed &&
176
+ t.isIdentifier(arg.property)),
177
+ );
178
+
179
+ const appendRequestArgument = (
180
+ callPath: { node: t.CallExpression | t.OptionalCallExpression },
181
+ requestExpression: t.Expression,
182
+ ) => {
183
+ if (hasExplicitRequestArgument(callPath.node.arguments)) return;
184
+ callPath.node.arguments.push(requestExpression);
185
+ };
186
+
187
+ const transformControllerFile = (filepath: string) => {
188
+ const code = fs.readFileSync(filepath, 'utf8');
189
+ const ast = parseModule(filepath, code);
190
+ const serviceBindings = new Set<string>();
191
+ let changed = false;
192
+
193
+ traverse(ast, {
194
+ VariableDeclarator(variablePath) {
195
+ if (!isServiceLikeBinding(variablePath.node.init as t.Expression | null | undefined)) return;
196
+ collectPatternNames(variablePath.node.id, serviceBindings);
197
+ },
198
+ CallExpression(callPath) {
199
+ const callee = callPath.node.callee;
200
+ if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) return;
201
+
202
+ const root = getMemberRoot(callee);
203
+ const isDirectServiceCall =
204
+ isThisMember(root, 'services') || isThisMember(root, 'app') || isThisMember(root, 'parent');
205
+ const isBoundServiceCall = t.isIdentifier(root) && serviceBindings.has(root.name);
206
+
207
+ if (!isDirectServiceCall && !isBoundServiceCall) return;
208
+
209
+ appendRequestArgument(callPath as { node: t.CallExpression }, t.memberExpression(t.thisExpression(), t.identifier('request')));
210
+ changed = true;
211
+ },
212
+ OptionalCallExpression(callPath) {
213
+ const callee = callPath.node.callee;
214
+ if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) return;
215
+
216
+ const root = getMemberRoot(callee);
217
+ const isDirectServiceCall =
218
+ isThisMember(root, 'services') || isThisMember(root, 'app') || isThisMember(root, 'parent');
219
+ const isBoundServiceCall = t.isIdentifier(root) && serviceBindings.has(root.name);
220
+
221
+ if (!isDirectServiceCall && !isBoundServiceCall) return;
222
+
223
+ appendRequestArgument(
224
+ callPath as { node: t.OptionalCallExpression },
225
+ t.memberExpression(t.thisExpression(), t.identifier('request')),
226
+ );
227
+ changed = true;
228
+ },
229
+ });
230
+
231
+ if (!changed) return;
232
+ fs.writeFileSync(filepath, generateModule(ast));
233
+ };
234
+
235
+ const ensureTypeImport = (ast: ReturnType<typeof parse>) => {
236
+ let hasTypeImport = false;
237
+ let serviceImport: t.ImportDeclaration | null = null;
238
+
239
+ for (const statement of ast.program.body) {
240
+ if (!t.isImportDeclaration(statement)) continue;
241
+ if (statement.source.value !== '@server/app/service') continue;
242
+
243
+ serviceImport = statement;
244
+ for (const specifier of statement.specifiers) {
245
+ if (
246
+ t.isImportSpecifier(specifier) &&
247
+ t.isIdentifier(specifier.imported, { name: 'TServiceRequestContext' })
248
+ ) {
249
+ hasTypeImport = true;
250
+ }
251
+ }
252
+ }
253
+
254
+ if (hasTypeImport) return;
255
+
256
+ if (serviceImport) {
257
+ serviceImport.specifiers.push(
258
+ t.importSpecifier(t.identifier('TServiceRequestContext'), t.identifier('TServiceRequestContext')),
259
+ );
260
+ return;
261
+ }
262
+
263
+ const importDeclaration = t.importDeclaration(
264
+ [t.importSpecifier(t.identifier('TServiceRequestContext'), t.identifier('TServiceRequestContext'))],
265
+ t.stringLiteral('@server/app/service'),
266
+ );
267
+ importDeclaration.importKind = 'type';
268
+ ast.program.body.unshift(importDeclaration);
269
+ };
270
+
271
+ const usesAmbientRequest = (methodPath: any) => {
272
+ let found = false;
273
+
274
+ methodPath.traverse({
275
+ MemberExpression(memberPath: any) {
276
+ if (isThisMember(memberPath.node, 'request')) {
277
+ found = true;
278
+ memberPath.stop();
279
+ }
280
+ },
281
+ OptionalMemberExpression(memberPath: any) {
282
+ if (isThisMember(memberPath.node, 'request')) {
283
+ found = true;
284
+ memberPath.stop();
285
+ }
286
+ },
287
+ });
288
+
289
+ return found;
290
+ };
291
+
292
+ const transformServiceFile = (filepath: string) => {
293
+ const code = fs.readFileSync(filepath, 'utf8');
294
+ const ast = parseModule(filepath, code);
295
+ let changed = false;
296
+
297
+ traverse(ast, {
298
+ ClassMethod(methodPath) {
299
+ if (!methodPath.node.body) return;
300
+ if (!usesAmbientRequest(methodPath)) return;
301
+
302
+ const hasRequestParam = methodPath.node.params.some(
303
+ (param) => t.isIdentifier(param) && param.name === 'request',
304
+ );
305
+ if (!hasRequestParam) {
306
+ const requestParam = t.identifier('request');
307
+ requestParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('TServiceRequestContext')));
308
+ methodPath.node.params.push(requestParam);
309
+ }
310
+
311
+ methodPath.traverse({
312
+ MemberExpression(memberPath) {
313
+ if (!isThisMember(memberPath.node, 'request')) return;
314
+ memberPath.replaceWith(t.identifier('request'));
315
+ },
316
+ OptionalMemberExpression(memberPath) {
317
+ if (!isThisMember(memberPath.node, 'request')) return;
318
+ memberPath.replaceWith(t.identifier('request'));
319
+ },
320
+ CallExpression(callPath) {
321
+ const callee = callPath.node.callee;
322
+ if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) return;
323
+
324
+ const root = getMemberRoot(callee);
325
+ if (!isThisMember(root, 'app') && !isThisMember(root, 'services') && !isThisMember(root, 'parent')) {
326
+ return;
327
+ }
328
+
329
+ appendRequestArgument(callPath as { node: t.CallExpression }, t.identifier('request'));
330
+ },
331
+ OptionalCallExpression(callPath) {
332
+ const callee = callPath.node.callee;
333
+ if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee)) return;
334
+
335
+ const root = getMemberRoot(callee);
336
+ if (!isThisMember(root, 'app') && !isThisMember(root, 'services') && !isThisMember(root, 'parent')) {
337
+ return;
338
+ }
339
+
340
+ appendRequestArgument(callPath as { node: t.OptionalCallExpression }, t.identifier('request'));
341
+ },
342
+ });
343
+
344
+ changed = true;
345
+ },
346
+ });
347
+
348
+ if (!changed) return;
349
+
350
+ ensureTypeImport(ast);
351
+ fs.writeFileSync(filepath, generateModule(ast));
352
+ };
353
+
354
+ const findFiles = (dir: string, predicate: (filepath: string) => boolean, results: string[] = []) => {
355
+ if (!fs.existsSync(dir)) return results;
356
+
357
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
358
+ const filepath = path.join(dir, dirent.name);
359
+
360
+ if (dirent.isDirectory()) {
361
+ findFiles(filepath, predicate, results);
362
+ continue;
363
+ }
364
+
365
+ if (dirent.isFile() && predicate(filepath)) {
366
+ results.push(filepath);
367
+ }
368
+ }
369
+
370
+ return results;
371
+ };
372
+
373
+ const moveControllers = (repoRoot: string) => {
374
+ const routeMap = parseControllerRoutes(repoRoot);
375
+
376
+ for (const [importPath, routeBasePath] of routeMap) {
377
+ const sourceFile = path.join(repoRoot, importPath.replace('@/', '') + '.ts');
378
+ const targetFile = path.join(repoRoot, 'server', 'controllers', ...routeBasePath.split('/')) + '.ts';
379
+
380
+ if (!fs.existsSync(sourceFile)) continue;
381
+
382
+ const content = fs.readFileSync(sourceFile, 'utf8');
383
+ const rewritten = rewriteRelativeImports(content, sourceFile, targetFile);
384
+
385
+ fs.ensureDirSync(path.dirname(targetFile));
386
+ fs.writeFileSync(targetFile, rewritten);
387
+ fs.removeSync(sourceFile);
388
+ transformControllerFile(targetFile);
389
+ }
390
+ };
391
+
392
+ const migrateServiceRequestUsage = (repoRoot: string) => {
393
+ const serviceFiles = findFiles(
394
+ path.join(repoRoot, 'server', 'services'),
395
+ (filepath) =>
396
+ /\.(ts|tsx)$/.test(filepath) &&
397
+ !filepath.endsWith('.controller.ts') &&
398
+ !filepath.includes(`${path.sep}router${path.sep}request.ts`),
399
+ );
400
+
401
+ for (const filepath of serviceFiles) {
402
+ const content = fs.readFileSync(filepath, 'utf8');
403
+ if (!content.includes('this.request')) continue;
404
+ if (content.includes('extends RequestService')) continue;
405
+
406
+ transformServiceFile(filepath);
407
+ }
408
+ };
409
+
410
+ const migrateRepo = (repoRoot: string) => {
411
+ moveControllers(repoRoot);
412
+ migrateServiceRequestUsage(repoRoot);
413
+ };
414
+
415
+ const repoRoots = process.argv.slice(2);
416
+
417
+ if (!repoRoots.length) {
418
+ throw new Error('Usage: ts-node scripts/migrate-explicit-controllers-and-request.ts <repo-root> [repo-root...]');
419
+ }
420
+
421
+ for (const repoRoot of repoRoots) {
422
+ migrateRepo(path.resolve(repoRoot));
423
+ }
@@ -66,7 +66,7 @@ const getRelativeServiceSegments = (serviceRootDir: string, filepath: string) =>
66
66
 
67
67
  const getControllerSegments = (relativePath: string) => {
68
68
  const segments = relativePath
69
- .replace(/\.controller\.ts$/, '')
69
+ .replace(/\.(tsx?|jsx?)$/, '')
70
70
  .split('/')
71
71
  .filter(Boolean);
72
72
 
@@ -77,14 +77,12 @@ const getControllerSegments = (relativePath: string) => {
77
77
  return segments;
78
78
  };
79
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
- }
80
+ const getControllerBasePath = (repoRoot: string, controllerFilepath: string) => {
81
+ const segments = getControllerSegments(
82
+ path.relative(path.join(repoRoot, 'server', 'controllers'), controllerFilepath).replace(/\\/g, '/'),
83
+ );
86
84
 
87
- return [serviceRoot.alias, ...segments].filter(Boolean).join('/');
85
+ return segments.join('/');
88
86
  };
89
87
 
90
88
  const findServiceAliases = (repoRoot: string) => {
@@ -254,7 +252,7 @@ const getControllerClassName = (serviceClassName: string, filepath: string) => {
254
252
  - TRANSFORM
255
253
  ----------------------------------*/
256
254
 
257
- const migrateServiceFile = (filepath: string, serviceRoots: TServiceRoot[]) => {
255
+ const migrateServiceFile = (filepath: string, serviceRoots: TServiceRoot[], repoRoot: string) => {
258
256
  const code = fs.readFileSync(filepath, 'utf8');
259
257
  if (!/@Route\(|from ['"]@app['"]|from ['"]@models['"]|from ['"]@request['"]/.test(code)) return false;
260
258
 
@@ -376,16 +374,18 @@ const migrateServiceFile = (filepath: string, serviceRoots: TServiceRoot[]) => {
376
374
  const serviceSegments = getRelativeServiceSegments(serviceRoot.dir, filepath);
377
375
  const serviceAccessPath = [serviceRoot.alias, ...serviceSegments].join('.');
378
376
 
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));
377
+ const controllerRelativePath = [serviceRoot.alias, ...serviceSegments].join('/');
378
+ const controllerFilepath = path.join(repoRoot, 'server', 'controllers', `${controllerRelativePath}.ts`);
379
+ const relativeServiceImportPath = path
380
+ .relative(path.dirname(controllerFilepath), filepath)
381
+ .replace(/\\/g, '/')
382
+ .replace(/\.(tsx?|jsx?)$/, '');
383
+ const normalizedServiceImportPath =
384
+ relativeServiceImportPath.startsWith('.') ? relativeServiceImportPath : `./${relativeServiceImportPath}`;
385
385
  const schemaImports = [...new Set(routeMethods.flatMap((routeMethod) => routeMethod.schemaImports))];
386
386
  const controllerClassName = getControllerClassName(serviceClassName, filepath);
387
387
  const needsSchemaHelperImport = routeMethods.some((routeMethod) => routeMethod.schemaSource?.includes('schema.'));
388
- const defaultControllerPath = getControllerBasePath(serviceRoot, controllerFilepath);
388
+ const defaultControllerPath = getControllerBasePath(repoRoot, controllerFilepath);
389
389
  const routeBasePaths = new Set<string>();
390
390
 
391
391
  for (const routeMethod of routeMethods) {
@@ -430,10 +430,11 @@ ${inputLine}
430
430
  })
431
431
  .join('\n\n');
432
432
 
433
+ fs.ensureDirSync(path.dirname(controllerFilepath));
433
434
  fs.writeFileSync(
434
435
  controllerFilepath,
435
436
  `import Controller${needsSchemaHelperImport ? ', { schema }' : ''} from '@server/app/controller';
436
- ${schemaImports.length ? `import { ${schemaImports.join(', ')} } from ${JSON.stringify(relativeServiceImportPath)};\n` : ''}
437
+ ${schemaImports.length ? `import { ${schemaImports.join(', ')} } from ${JSON.stringify(normalizedServiceImportPath)};\n` : ''}
437
438
  ${controllerPathExport}
438
439
  export default class ${controllerClassName} extends Controller {
439
440
 
@@ -463,7 +464,7 @@ for (const repoRoot of repoRoots) {
463
464
  let migratedFiles = 0;
464
465
 
465
466
  for (const serviceFile of serviceFiles) {
466
- if (migrateServiceFile(serviceFile, serviceRoots)) migratedFiles++;
467
+ if (migrateServiceFile(serviceFile, serviceRoots, repoRoot)) migratedFiles++;
467
468
  }
468
469
 
469
470
  console.log(`[refactor-server-controllers] ${repoRoot}: migrated ${migratedFiles} service files`);
@@ -273,7 +273,7 @@ if (!repoRoots.length)
273
273
 
274
274
  for (const repoRoot of repoRoots) {
275
275
  const serviceRoot = path.join(repoRoot, 'server', 'services');
276
- const files = findFiles(serviceRoot).filter((filepath) => !filepath.endsWith('.controller.ts'));
276
+ const files = findFiles(serviceRoot);
277
277
 
278
278
  let changedFiles = 0;
279
279
  let replacementCount = 0;