proteum 2.4.4 → 2.5.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.
- package/README.md +81 -52
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +16 -6
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/docs/migration-2.5.md +226 -0
- package/eslint.js +89 -42
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +120 -3
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +50 -41
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -31
- package/tests/eslint-rules.test.cjs +185 -8
- package/tests/mcp.test.cjs +90 -0
- package/tests/scaffold-templates.test.cjs +18 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -1,423 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { parse } from '@babel/parser';
|
|
4
|
-
import traverse, { NodePath } from '@babel/traverse';
|
|
5
|
-
import generate from '@babel/generator';
|
|
6
|
-
import * as types from '@babel/types';
|
|
7
|
-
|
|
8
|
-
const findFiles = (dir: string): string[] => {
|
|
9
|
-
if (!fs.existsSync(dir)) return [];
|
|
10
|
-
|
|
11
|
-
const files: string[] = [];
|
|
12
|
-
|
|
13
|
-
for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
14
|
-
const filepath = path.join(dir, dirent.name);
|
|
15
|
-
|
|
16
|
-
if (dirent.isDirectory()) {
|
|
17
|
-
files.push(...findFiles(filepath));
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (dirent.isFile() && /\.(tsx?|jsx?)$/.test(filepath)) files.push(filepath);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return files;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const isFunctionLikePath = (path: NodePath) =>
|
|
28
|
-
path.isFunctionDeclaration() ||
|
|
29
|
-
path.isFunctionExpression() ||
|
|
30
|
-
path.isArrowFunctionExpression() ||
|
|
31
|
-
path.isObjectMethod() ||
|
|
32
|
-
path.isClassMethod();
|
|
33
|
-
|
|
34
|
-
const getOutermostFunctionPath = (path: NodePath) => {
|
|
35
|
-
let current: NodePath | null = path;
|
|
36
|
-
let lastFunctionPath: NodePath | null = null;
|
|
37
|
-
|
|
38
|
-
while (current && !current.isProgram()) {
|
|
39
|
-
if (isFunctionLikePath(current)) lastFunctionPath = current;
|
|
40
|
-
|
|
41
|
-
current = current.parentPath;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return lastFunctionPath;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const ensureUseContextImport = (programPath: NodePath<types.Program>) => {
|
|
48
|
-
for (const statement of programPath.node.body) {
|
|
49
|
-
if (
|
|
50
|
-
statement.type === 'ImportDeclaration' &&
|
|
51
|
-
statement.source.value === '@/client/context' &&
|
|
52
|
-
statement.specifiers.some(
|
|
53
|
-
(specifier) => specifier.type === 'ImportDefaultSpecifier' && specifier.local.name === 'useContext',
|
|
54
|
-
)
|
|
55
|
-
)
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
programPath.unshiftContainer(
|
|
60
|
-
'body',
|
|
61
|
-
types.importDeclaration(
|
|
62
|
-
[types.importDefaultSpecifier(types.identifier('useContext'))],
|
|
63
|
-
types.stringLiteral('@/client/context'),
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const ensureBlockBody = (functionPath: NodePath) => {
|
|
69
|
-
if (functionPath.isArrowFunctionExpression() && functionPath.node.body.type !== 'BlockStatement') {
|
|
70
|
-
functionPath.node.body = types.blockStatement([types.returnStatement(functionPath.node.body)]);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return functionPath.get('body') as NodePath<types.BlockStatement>;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const hasExistingUseContextDeclaration = (bodyPath: NodePath<types.BlockStatement>, names: string[]) => {
|
|
77
|
-
return bodyPath.node.body.some(
|
|
78
|
-
(statement) =>
|
|
79
|
-
statement.type === 'VariableDeclaration' &&
|
|
80
|
-
statement.declarations.some(
|
|
81
|
-
(declaration) =>
|
|
82
|
-
declaration.id.type === 'ObjectPattern' &&
|
|
83
|
-
declaration.init?.type === 'CallExpression' &&
|
|
84
|
-
declaration.init.callee.type === 'Identifier' &&
|
|
85
|
-
declaration.init.callee.name === 'useContext' &&
|
|
86
|
-
names.every((name) =>
|
|
87
|
-
declaration.id.properties.some(
|
|
88
|
-
(property) =>
|
|
89
|
-
property.type === 'ObjectProperty' &&
|
|
90
|
-
property.key.type === 'Identifier' &&
|
|
91
|
-
property.key.name === name,
|
|
92
|
-
),
|
|
93
|
-
),
|
|
94
|
-
),
|
|
95
|
-
);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const objectPatternHasProperty = (pattern: types.ObjectPattern, localName: string) =>
|
|
99
|
-
pattern.properties.some(
|
|
100
|
-
(property) =>
|
|
101
|
-
property.type === 'ObjectProperty' &&
|
|
102
|
-
property.value.type === 'Identifier' &&
|
|
103
|
-
property.value.name === localName,
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const addObjectPatternProperty = (pattern: types.ObjectPattern, keyName: string, localName: string = keyName) => {
|
|
107
|
-
if (objectPatternHasProperty(pattern, localName)) return;
|
|
108
|
-
|
|
109
|
-
pattern.properties.push(
|
|
110
|
-
types.objectProperty(types.identifier(keyName), types.identifier(localName), false, keyName === localName),
|
|
111
|
-
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const getUseContextStatements = (bodyPath: NodePath<types.BlockStatement>) => {
|
|
115
|
-
return bodyPath
|
|
116
|
-
.get('body')
|
|
117
|
-
.filter(
|
|
118
|
-
(statementPath) =>
|
|
119
|
-
statementPath.isVariableDeclaration() &&
|
|
120
|
-
statementPath.node.declarations.length === 1 &&
|
|
121
|
-
statementPath.node.declarations[0].id.type === 'ObjectPattern' &&
|
|
122
|
-
statementPath.node.declarations[0].init?.type === 'CallExpression' &&
|
|
123
|
-
statementPath.node.declarations[0].init.callee.type === 'Identifier' &&
|
|
124
|
-
statementPath.node.declarations[0].init.callee.name === 'useContext',
|
|
125
|
-
) as NodePath<types.VariableDeclaration>[];
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const repoRoots = process.argv.slice(2);
|
|
129
|
-
if (!repoRoots.length)
|
|
130
|
-
throw new Error('Usage: ts-node scripts/refactor-client-app-imports.ts <repo-root> [repo-root...]');
|
|
131
|
-
|
|
132
|
-
for (const repoRoot of repoRoots) {
|
|
133
|
-
const clientRoot = path.join(repoRoot, 'client');
|
|
134
|
-
const files = findFiles(clientRoot).filter((filepath) => !filepath.includes('/client/pages/'));
|
|
135
|
-
let changedFiles = 0;
|
|
136
|
-
|
|
137
|
-
for (const filepath of files) {
|
|
138
|
-
const code = fs.readFileSync(filepath, 'utf8');
|
|
139
|
-
if (!code.includes('@app') && !code.includes('"@app"')) continue;
|
|
140
|
-
|
|
141
|
-
const ast = parse(code, {
|
|
142
|
-
sourceType: 'module',
|
|
143
|
-
plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties'],
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const importedNames = new Set<string>();
|
|
147
|
-
const functionBindings = new Map<NodePath, Set<string>>();
|
|
148
|
-
let hasAppImport = false;
|
|
149
|
-
|
|
150
|
-
traverse(ast, {
|
|
151
|
-
ImportDeclaration(path) {
|
|
152
|
-
if (path.node.source.value !== '@app') return;
|
|
153
|
-
|
|
154
|
-
hasAppImport = true;
|
|
155
|
-
|
|
156
|
-
for (const specifier of path.node.specifiers) {
|
|
157
|
-
if (specifier.type === 'ImportSpecifier' && specifier.imported.type === 'Identifier')
|
|
158
|
-
importedNames.add(specifier.local.name);
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
Identifier(path) {
|
|
162
|
-
if (!importedNames.has(path.node.name)) return;
|
|
163
|
-
|
|
164
|
-
if (!path.isReferencedIdentifier()) return;
|
|
165
|
-
|
|
166
|
-
const binding = path.scope.getBinding(path.node.name);
|
|
167
|
-
if (!binding?.path.isImportSpecifier()) return;
|
|
168
|
-
|
|
169
|
-
const functionPath = getOutermostFunctionPath(path);
|
|
170
|
-
if (!functionPath) return;
|
|
171
|
-
|
|
172
|
-
const names = functionBindings.get(functionPath) || new Set<string>();
|
|
173
|
-
names.add(path.node.name);
|
|
174
|
-
functionBindings.set(functionPath, names);
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
if (!hasAppImport) continue;
|
|
179
|
-
|
|
180
|
-
traverse(ast, {
|
|
181
|
-
Program(programPath) {
|
|
182
|
-
for (const statementPath of programPath.get('body')) {
|
|
183
|
-
if (!statementPath.isImportDeclaration()) continue;
|
|
184
|
-
|
|
185
|
-
if (statementPath.node.source.value === '@app') statementPath.remove();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
ensureUseContextImport(programPath);
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
for (const [functionPath, namesSet] of functionBindings) {
|
|
193
|
-
const names = [...namesSet].sort((a, b) => a.localeCompare(b));
|
|
194
|
-
const bodyPath = ensureBlockBody(functionPath);
|
|
195
|
-
const useContextStatements = getUseContextStatements(bodyPath);
|
|
196
|
-
|
|
197
|
-
if (hasExistingUseContextDeclaration(bodyPath, names)) continue;
|
|
198
|
-
|
|
199
|
-
if (useContextStatements.length) {
|
|
200
|
-
const primaryDeclaration = useContextStatements[0].node.declarations[0];
|
|
201
|
-
const primaryPattern = primaryDeclaration.id as types.ObjectPattern;
|
|
202
|
-
|
|
203
|
-
for (const statementPath of useContextStatements.slice(1)) {
|
|
204
|
-
const declaration = statementPath.node.declarations[0];
|
|
205
|
-
const pattern = declaration.id as types.ObjectPattern;
|
|
206
|
-
|
|
207
|
-
for (const property of pattern.properties) {
|
|
208
|
-
if (
|
|
209
|
-
property.type === 'ObjectProperty' &&
|
|
210
|
-
property.key.type === 'Identifier' &&
|
|
211
|
-
property.value.type === 'Identifier'
|
|
212
|
-
)
|
|
213
|
-
addObjectPatternProperty(primaryPattern, property.key.name, property.value.name);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
statementPath.remove();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const name of names) addObjectPatternProperty(primaryPattern, name);
|
|
220
|
-
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
bodyPath.unshiftContainer(
|
|
225
|
-
'body',
|
|
226
|
-
types.variableDeclaration('const', [
|
|
227
|
-
types.variableDeclarator(
|
|
228
|
-
types.objectPattern(
|
|
229
|
-
names.map((name) =>
|
|
230
|
-
types.objectProperty(types.identifier(name), types.identifier(name), false, true),
|
|
231
|
-
),
|
|
232
|
-
),
|
|
233
|
-
types.callExpression(types.identifier('useContext'), []),
|
|
234
|
-
),
|
|
235
|
-
]),
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
fs.writeFileSync(filepath, generate(ast, {}, code).code);
|
|
240
|
-
changedFiles++;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
console.log(`[refactor-client-app-imports] ${repoRoot}: changed ${changedFiles} files`);
|
|
244
|
-
}
|