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.
- package/AGENTS.md +101 -0
- package/agents/codex/AGENTS.md +95 -0
- package/agents/codex/CODING_STYLE.md +71 -0
- package/agents/codex/agents.md.zip +0 -0
- package/agents/codex/client/AGENTS.md +102 -0
- package/agents/codex/client/pages/AGENTS.md +35 -0
- package/agents/codex/server/routes/AGENTS.md +12 -0
- package/agents/codex/server/services/AGENTS.md +137 -0
- package/agents/codex/tests/AGENTS.md +8 -0
- package/cli/app/config.ts +13 -11
- package/cli/app/index.ts +74 -82
- package/cli/bin.js +1 -1
- package/cli/commands/build.ts +51 -14
- package/cli/commands/check.ts +19 -0
- package/cli/commands/deploy/app.ts +4 -8
- package/cli/commands/deploy/web.ts +16 -20
- package/cli/commands/dev.ts +189 -64
- package/cli/commands/devEvents.ts +106 -0
- package/cli/commands/init.ts +63 -57
- package/cli/commands/lint.ts +21 -0
- package/cli/commands/refresh.ts +18 -0
- package/cli/commands/typecheck.ts +18 -0
- package/cli/compiler/client/identite.ts +80 -53
- package/cli/compiler/client/index.ts +139 -213
- package/cli/compiler/common/bundleAnalysis.ts +94 -0
- package/cli/compiler/common/clientManifest.ts +67 -0
- package/cli/compiler/common/controllers.ts +288 -0
- package/cli/compiler/common/files/autres.ts +7 -18
- package/cli/compiler/common/files/images.ts +40 -37
- package/cli/compiler/common/files/style.ts +11 -22
- package/cli/compiler/common/generatedRouteModules.ts +368 -0
- package/cli/compiler/common/index.ts +31 -65
- package/cli/compiler/common/loaders/forbid-ssr-import.js +13 -0
- package/cli/compiler/common/rspackAliases.ts +13 -0
- package/cli/compiler/common/scripts.ts +37 -0
- package/cli/compiler/index.ts +781 -230
- package/cli/compiler/server/index.ts +59 -75
- package/cli/compiler/writeIfChanged.ts +21 -0
- package/cli/index.ts +71 -72
- package/cli/paths.ts +51 -57
- package/cli/print.ts +17 -11
- package/cli/tsconfig.json +5 -4
- package/cli/utils/agents.ts +100 -0
- package/cli/utils/check.ts +71 -0
- package/cli/utils/index.ts +1 -3
- package/cli/utils/keyboard.ts +8 -25
- package/cli/utils/runProcess.ts +30 -0
- package/client/app/component.tsx +29 -29
- package/client/app/index.ts +36 -57
- package/client/app/service.ts +7 -12
- package/client/app.tsconfig.json +2 -2
- package/client/components/Dialog/Manager.ssr.tsx +40 -0
- package/client/components/Dialog/Manager.tsx +119 -150
- package/client/components/Dialog/status.tsx +3 -3
- package/client/components/index.ts +1 -1
- package/client/components/types.d.ts +1 -3
- package/client/dev/hmr.ts +65 -0
- package/client/global.d.ts +2 -2
- package/client/hooks.ts +6 -9
- package/client/index.ts +2 -1
- package/client/islands/index.ts +7 -0
- package/client/islands/useDeferredModule.ts +199 -0
- package/client/pages/_layout/index.tsx +4 -12
- package/client/pages/useHeader.tsx +14 -21
- package/client/router.ts +27 -0
- package/client/services/router/components/Link.tsx +34 -27
- package/client/services/router/components/Page.tsx +6 -14
- package/client/services/router/components/router.ssr.tsx +36 -0
- package/client/services/router/components/router.tsx +63 -83
- package/client/services/router/index.tsx +185 -220
- package/client/services/router/request/api.ts +97 -119
- package/client/services/router/request/history.ts +2 -2
- package/client/services/router/request/index.ts +13 -12
- package/client/services/router/request/multipart.ts +72 -62
- package/client/services/router/response/index.tsx +68 -61
- package/client/services/router/response/page.ts +28 -32
- package/client/utils/dom.ts +17 -33
- package/common/app/index.ts +3 -3
- package/common/data/chaines/index.ts +22 -23
- package/common/data/dates.ts +35 -70
- package/common/data/markdown.ts +42 -39
- package/common/dev/serverHotReload.ts +26 -0
- package/common/errors/index.tsx +110 -142
- package/common/router/contracts.ts +29 -0
- package/common/router/index.ts +89 -108
- package/common/router/layouts.ts +34 -47
- package/common/router/pageSetup.ts +50 -0
- package/common/router/register.ts +53 -24
- package/common/router/request/api.ts +30 -36
- package/common/router/request/index.ts +2 -8
- package/common/router/response/index.ts +8 -15
- package/common/router/response/page.ts +70 -58
- package/common/utils.ts +1 -1
- package/doc/TODO.md +1 -1
- package/eslint.js +62 -0
- package/package.json +14 -49
- package/prettier.config.cjs +9 -0
- package/scripts/cleanup-generated-controllers.ts +62 -0
- package/scripts/fix-reference-app-typing.ts +490 -0
- package/scripts/refactor-client-app-imports.ts +244 -0
- package/scripts/refactor-client-pages.ts +587 -0
- package/scripts/refactor-server-controllers.ts +470 -0
- package/scripts/refactor-server-runtime-aliases.ts +360 -0
- package/scripts/restore-client-app-import-files.ts +41 -0
- package/scripts/restore-files-from-git-head.ts +20 -0
- package/scripts/update-codex-agents.ts +35 -0
- package/server/app/commands.ts +35 -64
- package/server/app/container/config.ts +48 -59
- package/server/app/container/console/index.ts +202 -248
- package/server/app/container/index.ts +33 -71
- package/server/app/controller/index.ts +61 -0
- package/server/app/index.ts +39 -105
- package/server/app/service/container.ts +41 -42
- package/server/app/service/index.ts +120 -147
- package/server/context.ts +1 -1
- package/server/index.ts +25 -1
- package/server/services/auth/index.ts +75 -115
- package/server/services/auth/router/index.ts +31 -32
- package/server/services/auth/router/request.ts +14 -16
- package/server/services/cron/CronTask.ts +13 -26
- package/server/services/cron/index.ts +14 -36
- package/server/services/disks/driver.ts +40 -58
- package/server/services/disks/drivers/local/index.ts +79 -90
- package/server/services/disks/drivers/s3/index.ts +116 -163
- package/server/services/disks/index.ts +23 -38
- package/server/services/email/index.ts +45 -104
- package/server/services/email/utils.ts +14 -27
- package/server/services/fetch/index.ts +53 -85
- package/server/services/prisma/Facet.ts +39 -91
- package/server/services/prisma/index.ts +74 -110
- package/server/services/router/generatedRuntime.ts +29 -0
- package/server/services/router/http/index.ts +78 -73
- package/server/services/router/http/multipart.ts +19 -42
- package/server/services/router/index.ts +378 -365
- package/server/services/router/request/api.ts +26 -25
- package/server/services/router/request/index.ts +44 -51
- package/server/services/router/request/service.ts +7 -11
- package/server/services/router/request/validation/zod.ts +111 -148
- package/server/services/router/response/index.ts +110 -125
- package/server/services/router/response/mask/Filter.ts +31 -72
- package/server/services/router/response/mask/index.ts +8 -15
- package/server/services/router/response/mask/selecteurs.ts +11 -25
- package/server/services/router/response/page/clientManifest.ts +25 -0
- package/server/services/router/response/page/document.tsx +199 -127
- package/server/services/router/response/page/index.tsx +89 -94
- package/server/services/router/service.ts +13 -15
- package/server/services/schema/index.ts +17 -26
- package/server/services/schema/request.ts +19 -33
- package/server/services/schema/router/index.ts +8 -11
- package/server/services/security/encrypt/aes/index.ts +15 -35
- package/server/utils/slug.ts +29 -35
- package/skills/clean-project-code/SKILL.md +63 -0
- package/skills/clean-project-code/agents/openai.yaml +4 -0
- package/tsconfig.common.json +4 -3
- package/tsconfig.json +4 -1
- package/types/aliases.d.ts +17 -21
- package/types/controller-input.test.ts +48 -0
- package/types/express-extra.d.ts +6 -0
- package/types/global/constants.d.ts +13 -0
- package/types/global/express-extra.d.ts +6 -0
- package/types/global/modules.d.ts +13 -16
- package/types/global/utils.d.ts +17 -49
- package/types/global/vendors.d.ts +62 -0
- package/types/icons.d.ts +65 -1
- package/types/uuid.d.ts +3 -0
- package/types/vendors.d.ts +62 -0
- package/cli/compiler/common/babel/index.ts +0 -170
- package/cli/compiler/common/babel/plugins/index.ts +0 -0
- package/cli/compiler/common/babel/plugins/services.ts +0 -586
- package/cli/compiler/common/babel/routes/imports.ts +0 -127
- package/cli/compiler/common/babel/routes/routes.ts +0 -1130
- package/client/services/captcha/index.ts +0 -67
- package/client/services/socket/index.ts +0 -147
- package/common/data/rte/nodes.ts +0 -83
- package/common/data/stats.ts +0 -90
- package/common/utils/rte.ts +0 -183
- package/server/services/auth/old.ts +0 -277
- package/server/services/cache/commands.ts +0 -41
- package/server/services/cache/index.ts +0 -297
- package/server/services/cache/service.json +0 -6
- package/server/services/socket/index.ts +0 -162
- package/server/services/socket/scope.ts +0 -226
- package/server/services/socket/service.json +0 -6
- package/server/services_old/SocketClient.ts +0 -92
- 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
|
+
}
|