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.
- package/AGENTS.md +13 -1
- package/README.md +375 -0
- package/agents/framework/AGENTS.md +917 -0
- package/agents/project/AGENTS.md +138 -0
- package/agents/{codex → project}/CODING_STYLE.md +3 -2
- package/agents/project/client/AGENTS.md +108 -0
- package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
- package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
- package/agents/project/server/services/AGENTS.md +170 -0
- package/agents/{codex → project}/tests/AGENTS.md +1 -0
- package/cli/app/config.ts +3 -2
- package/cli/app/index.ts +6 -66
- package/cli/bin.js +7 -2
- package/cli/commands/build.ts +94 -27
- package/cli/commands/check.ts +15 -1
- package/cli/commands/dev.ts +288 -132
- package/cli/commands/doctor.ts +108 -0
- package/cli/commands/explain.ts +226 -0
- package/cli/commands/init.ts +76 -70
- package/cli/commands/lint.ts +18 -1
- package/cli/commands/refresh.ts +16 -6
- package/cli/commands/typecheck.ts +14 -1
- package/cli/compiler/artifacts/controllers.ts +150 -0
- package/cli/compiler/artifacts/discovery.ts +132 -0
- package/cli/compiler/artifacts/manifest.ts +267 -0
- package/cli/compiler/artifacts/routing.ts +315 -0
- package/cli/compiler/artifacts/services.ts +480 -0
- package/cli/compiler/artifacts/shared.ts +12 -0
- package/cli/compiler/client/identite.ts +2 -1
- package/cli/compiler/client/index.ts +13 -3
- package/cli/compiler/common/controllers.ts +23 -28
- package/cli/compiler/common/files/style.ts +3 -4
- package/cli/compiler/common/generatedRouteModules.ts +333 -19
- package/cli/compiler/common/proteumManifest.ts +133 -0
- package/cli/compiler/index.ts +33 -896
- package/cli/compiler/server/index.ts +21 -4
- package/cli/context.ts +71 -0
- package/cli/index.ts +39 -181
- package/cli/presentation/commands.ts +208 -0
- package/cli/presentation/compileReporter.ts +65 -0
- package/cli/presentation/devSession.ts +70 -0
- package/cli/presentation/help.ts +193 -0
- package/cli/presentation/ink.ts +69 -0
- package/cli/presentation/layout.ts +83 -0
- package/cli/runtime/argv.ts +49 -0
- package/cli/runtime/command.ts +25 -0
- package/cli/runtime/commands.ts +221 -0
- package/cli/runtime/importEsm.ts +7 -0
- package/cli/runtime/verbose.ts +15 -0
- package/cli/utils/agents.ts +5 -4
- package/cli/utils/keyboard.ts +12 -6
- package/client/app/index.ts +0 -6
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/response/index.tsx +2 -2
- package/common/dev/serverHotReload.ts +12 -0
- package/common/router/index.ts +3 -2
- package/common/router/layouts.ts +1 -1
- package/common/router/pageSetup.ts +1 -0
- package/package.json +10 -8
- package/prettier/router-registration-plugin.cjs +52 -0
- package/prettier.config.cjs +1 -0
- package/scripts/cleanup-generated-controllers.ts +2 -2
- package/scripts/fix-reference-app-typing.ts +2 -2
- package/scripts/format-router-registrations.ts +119 -0
- package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
- package/scripts/refactor-server-controllers.ts +19 -18
- package/scripts/refactor-server-runtime-aliases.ts +1 -1
- package/server/app/commands.ts +309 -25
- package/server/app/container/config.ts +1 -1
- package/server/app/container/index.ts +2 -2
- package/server/app/controller/index.ts +13 -4
- package/server/app/index.ts +53 -37
- package/server/app/service/container.ts +26 -28
- package/server/app/service/index.ts +10 -20
- package/server/app.tsconfig.json +9 -2
- package/server/index.ts +32 -1
- package/server/services/auth/index.ts +234 -15
- package/server/services/auth/router/index.ts +39 -7
- package/server/services/auth/router/request.ts +40 -8
- package/server/services/disks/index.ts +1 -1
- package/server/services/prisma/Facet.ts +2 -2
- package/server/services/prisma/index.ts +22 -5
- package/server/services/prisma/mariadb.ts +47 -0
- package/server/services/router/http/index.ts +9 -1
- package/server/services/router/index.ts +10 -4
- package/server/services/router/response/index.ts +26 -6
- package/types/auth-check-rules.test.ts +51 -0
- package/types/controller-request-context.test.ts +55 -0
- package/types/service-config.test.ts +39 -0
- package/agents/codex/AGENTS.md +0 -95
- package/agents/codex/client/AGENTS.md +0 -102
- package/agents/codex/server/services/AGENTS.md +0 -137
- package/server/services/models.7z +0 -0
- /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(/\.
|
|
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 = (
|
|
81
|
-
const segments = getControllerSegments(
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
276
|
+
const files = findFiles(serviceRoot);
|
|
277
277
|
|
|
278
278
|
let changedFiles = 0;
|
|
279
279
|
let replacementCount = 0;
|