proteum 2.4.4 → 2.5.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/README.md +60 -55
- 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 +1 -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/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/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +14 -4
- 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/eslint.js +36 -1
- 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 +92 -1
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +47 -38
- 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/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -28
- package/tests/eslint-rules.test.cjs +39 -1
- package/tests/mcp.test.cjs +90 -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,490 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import ts from 'typescript';
|
|
4
|
-
|
|
5
|
-
const ROOT = '/Users/gaetan/Desktop/Projets';
|
|
6
|
-
const UNIQUE_ROOT = path.join(ROOT, 'unique.domains/website');
|
|
7
|
-
const CROSSPATH_ROOT = path.join(ROOT, 'crosspath/platform');
|
|
8
|
-
|
|
9
|
-
const TARGET_CONTEXT_NAMES = new Set([
|
|
10
|
-
'Investor',
|
|
11
|
-
'Crm',
|
|
12
|
-
'Prospect',
|
|
13
|
-
'Headhunters',
|
|
14
|
-
'Founder',
|
|
15
|
-
'Domains',
|
|
16
|
-
'Navigation',
|
|
17
|
-
'Router',
|
|
18
|
-
'Users',
|
|
19
|
-
'Plans',
|
|
20
|
-
'Auth',
|
|
21
|
-
'Admin',
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
type TLogConfig = { filepath: string; baseDir: string };
|
|
25
|
-
|
|
26
|
-
const LOGS: TLogConfig[] = [
|
|
27
|
-
{ filepath: '/tmp/unique-client-after-page-contract.log', baseDir: path.join(UNIQUE_ROOT, 'client') },
|
|
28
|
-
{ filepath: '/tmp/unique-server-after-page-contract.log', baseDir: path.join(UNIQUE_ROOT, 'server') },
|
|
29
|
-
{ filepath: '/tmp/crosspath-client-after-page-contract.log', baseDir: path.join(CROSSPATH_ROOT, 'client') },
|
|
30
|
-
{ filepath: '/tmp/crosspath-server-after-page-contract.log', baseDir: path.join(CROSSPATH_ROOT, 'server') },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const assertReplace = (filepath: string, source: string, searchValue: string, replaceValue: string) => {
|
|
34
|
-
if (!source.includes(searchValue)) throw new Error(`Could not find expected content in ${filepath}: ${searchValue}`);
|
|
35
|
-
|
|
36
|
-
return source.replace(searchValue, replaceValue);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const ensureContextBinding = (source: string, bindingName: string) => {
|
|
40
|
-
if (
|
|
41
|
-
source.includes(`const { ${bindingName} } = context;`) ||
|
|
42
|
-
source.includes(`const { ${bindingName} } = useContext();`)
|
|
43
|
-
) {
|
|
44
|
-
return source;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return source.replace(
|
|
48
|
-
' const context = useContext();',
|
|
49
|
-
` const context = useContext();\n const { ${bindingName} } = context;`,
|
|
50
|
-
);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const ensureContextHookBinding = (source: string, bindingName: string, anchor: string) => {
|
|
54
|
-
if (
|
|
55
|
-
source.includes(`const { ${bindingName} } = useContext();`) ||
|
|
56
|
-
source.includes(`const { ${bindingName} } = context;`)
|
|
57
|
-
) {
|
|
58
|
-
return source;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return source.replace(anchor, ` const { ${bindingName} } = useContext();\n${anchor}`);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const writeIfChanged = (filepath: string, nextContent: string) => {
|
|
65
|
-
const currentContent = fs.readFileSync(filepath, 'utf8');
|
|
66
|
-
if (currentContent === nextContent) return false;
|
|
67
|
-
|
|
68
|
-
fs.writeFileSync(filepath, nextContent);
|
|
69
|
-
console.info(`updated ${filepath}`);
|
|
70
|
-
return true;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const applyLiteralReplacement = (filepath: string, updater: (source: string) => string) => {
|
|
74
|
-
const currentContent = fs.readFileSync(filepath, 'utf8');
|
|
75
|
-
const nextContent = updater(currentContent);
|
|
76
|
-
if (nextContent !== currentContent) {
|
|
77
|
-
fs.writeFileSync(filepath, nextContent);
|
|
78
|
-
console.info(`updated ${filepath}`);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const patchCentralFiles = () => {
|
|
83
|
-
applyLiteralReplacement(path.join(CROSSPATH_ROOT, 'client/components/legacy/Button.tsx'), (source) =>
|
|
84
|
-
source.includes('export type TLinkProps = React.JSX.HTMLAttributes<HTMLAnchorElement> & { link: string };')
|
|
85
|
-
? source
|
|
86
|
-
: assertReplace(
|
|
87
|
-
path.join(CROSSPATH_ROOT, 'client/components/legacy/Button.tsx'),
|
|
88
|
-
source,
|
|
89
|
-
'export type TLinkProps = React.JSX.HTMLAttributes<HTMLAnchorElement>;',
|
|
90
|
-
'export type TLinkProps = React.JSX.HTMLAttributes<HTMLAnchorElement> & { link: string };',
|
|
91
|
-
),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
applyLiteralReplacement(path.join(CROSSPATH_ROOT, 'client/pages/_messages/401.tsx'), (source) =>
|
|
95
|
-
source.includes(' Router.go(loginPage);')
|
|
96
|
-
? source
|
|
97
|
-
: assertReplace(
|
|
98
|
-
path.join(CROSSPATH_ROOT, 'client/pages/_messages/401.tsx'),
|
|
99
|
-
source,
|
|
100
|
-
' page?.go(loginPage);',
|
|
101
|
-
' Router.go(loginPage);',
|
|
102
|
-
),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
applyLiteralReplacement(path.join(UNIQUE_ROOT, 'client/pages/_messages/ErrorScreen.tsx'), (source) =>
|
|
106
|
-
source
|
|
107
|
-
.replace(
|
|
108
|
-
'{ ArrowRight, Home, LifeBuoy, LogIn, RefreshCw } from "lucide-preact";',
|
|
109
|
-
'{ ArrowRight, Home, LifeBuoy, LogIn, RefreshCw, type LucideIcon } from "lucide-preact";',
|
|
110
|
-
)
|
|
111
|
-
.replace(
|
|
112
|
-
'type TLucideIcon = React.ComponentType<{ size?: number | string; className?: string }>;',
|
|
113
|
-
'type TLucideIcon = LucideIcon;',
|
|
114
|
-
),
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
applyLiteralReplacement(path.join(UNIQUE_ROOT, 'client/pages/Investor/database/DatabasePage.tsx'), (source) =>
|
|
118
|
-
source.includes('const { page: serverPage, user, Router, Investor, Domains } = useContext();')
|
|
119
|
-
? source
|
|
120
|
-
: assertReplace(
|
|
121
|
-
path.join(UNIQUE_ROOT, 'client/pages/Investor/database/DatabasePage.tsx'),
|
|
122
|
-
source,
|
|
123
|
-
' const { page: serverPage, user, Router } = useContext();',
|
|
124
|
-
' const { page: serverPage, user, Router, Investor, Domains } = useContext();',
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
applyLiteralReplacement(path.join(UNIQUE_ROOT, 'client/pages/Investor/insights/index.tsx'), (source) =>
|
|
129
|
-
source.includes(' const { Investor } = useAppContext();\n const [byGroupBy, setByGroupBy]')
|
|
130
|
-
? source
|
|
131
|
-
: assertReplace(
|
|
132
|
-
path.join(UNIQUE_ROOT, 'client/pages/Investor/insights/index.tsx'),
|
|
133
|
-
source,
|
|
134
|
-
`}) => {
|
|
135
|
-
const [byGroupBy, setByGroupBy] = React.useState<InsightsRadarLensState>(() =>
|
|
136
|
-
emptyRadarLensState(),
|
|
137
|
-
);
|
|
138
|
-
`,
|
|
139
|
-
`}) => {
|
|
140
|
-
const { Investor } = useAppContext();
|
|
141
|
-
const [byGroupBy, setByGroupBy] = React.useState<InsightsRadarLensState>(() =>
|
|
142
|
-
emptyRadarLensState(),
|
|
143
|
-
);
|
|
144
|
-
`,
|
|
145
|
-
),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
applyLiteralReplacement(
|
|
149
|
-
path.join(UNIQUE_ROOT, 'client/pages/Investor/layout/components/user-settings/sections/GeneralSettingsSection.tsx'),
|
|
150
|
-
(source) =>
|
|
151
|
-
ensureContextBinding(
|
|
152
|
-
source.replace(
|
|
153
|
-
'type UpdateProfileResponse = Awaited<ReturnType<typeof Users.updateProfile>>;',
|
|
154
|
-
`type UpdateProfileResponse = {
|
|
155
|
-
ok?: boolean;
|
|
156
|
-
firstName?: string | null;
|
|
157
|
-
lastName?: string | null;
|
|
158
|
-
name?: string | null;
|
|
159
|
-
};`,
|
|
160
|
-
),
|
|
161
|
-
'Users',
|
|
162
|
-
),
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
applyLiteralReplacement(
|
|
166
|
-
path.join(UNIQUE_ROOT, 'client/pages/Investor/layout/components/user-settings/sections/PlanSettingsSection.tsx'),
|
|
167
|
-
(source) =>
|
|
168
|
-
ensureContextBinding(
|
|
169
|
-
source.replace('type PlansById = Awaited<ReturnType<typeof Plans.getPlans>>;', 'type PlansById = Record<string, any>;'),
|
|
170
|
-
'Plans',
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
applyLiteralReplacement(
|
|
175
|
-
path.join(UNIQUE_ROOT, 'client/pages/Investor/layout/components/user-settings/sections/SecuritySettingsSection.tsx'),
|
|
176
|
-
(source) =>
|
|
177
|
-
ensureContextHookBinding(
|
|
178
|
-
source
|
|
179
|
-
.replace(
|
|
180
|
-
'// App\n\nimport Icon from "@/client/components/Icon";',
|
|
181
|
-
'// App\n\nimport Icon from "@/client/components/Icon";\nimport useContext from "@/client/context";',
|
|
182
|
-
)
|
|
183
|
-
.replace(
|
|
184
|
-
'type SecuritySummary = Awaited<ReturnType<typeof Auth.getSecuritySummary>>;',
|
|
185
|
-
`type SecuritySummary = {
|
|
186
|
-
authMethod?: string | null;
|
|
187
|
-
hasPassword?: boolean;
|
|
188
|
-
lastLogin?: string | Date | null;
|
|
189
|
-
lastLoginIP?: string | null;
|
|
190
|
-
connectedProviders?: Array<{ provider?: string | null; email?: string | null }>;
|
|
191
|
-
trustedDevices?: Array<{ deviceName?: string | null; userAgent?: string | null; lastSeen?: string | Date | null }>;
|
|
192
|
-
};`,
|
|
193
|
-
),
|
|
194
|
-
'Auth',
|
|
195
|
-
' const [state, setState] = React.useState<LoadState>("idle");',
|
|
196
|
-
),
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
applyLiteralReplacement(path.join(UNIQUE_ROOT, 'server/controllers/Domains/search.ts'), (source) =>
|
|
200
|
-
source.includes(' public async GetLandingSeoCachedSearches() {')
|
|
201
|
-
? source
|
|
202
|
-
: assertReplace(
|
|
203
|
-
path.join(UNIQUE_ROOT, 'server/controllers/Domains/search.ts'),
|
|
204
|
-
source,
|
|
205
|
-
` public async GetLandingSeoCachedSearch() {
|
|
206
|
-
const { Domains } = this.services;
|
|
207
|
-
const data = this.input(schema.object({ "searchHash": schema.string() }));
|
|
208
|
-
return Domains.search.GetLandingSeoCachedSearch(data);
|
|
209
|
-
}
|
|
210
|
-
`,
|
|
211
|
-
` public async GetLandingSeoCachedSearches() {
|
|
212
|
-
const { Domains } = this.services;
|
|
213
|
-
return Domains.search.GetLandingSeoCachedSearches();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
public async GetLandingSeoCachedSearch() {
|
|
217
|
-
const { Domains } = this.services;
|
|
218
|
-
const data = this.input(schema.object({ "searchHash": schema.string() }));
|
|
219
|
-
return Domains.search.GetLandingSeoCachedSearch(data);
|
|
220
|
-
}
|
|
221
|
-
`,
|
|
222
|
-
),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
applyLiteralReplacement(path.join(UNIQUE_ROOT, 'client/pages/Founder/setup/index.tsx'), (source) =>
|
|
226
|
-
source.includes(' const mutate = async (promise: PromiseLike<FounderSetupMutationResponse>) => {')
|
|
227
|
-
? source
|
|
228
|
-
: assertReplace(
|
|
229
|
-
path.join(UNIQUE_ROOT, 'client/pages/Founder/setup/index.tsx'),
|
|
230
|
-
source,
|
|
231
|
-
' const mutate = async (promise: Promise<FounderSetupMutationResponse>) => {',
|
|
232
|
-
' const mutate = async (promise: PromiseLike<FounderSetupMutationResponse>) => {',
|
|
233
|
-
),
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
for (const relativePath of [
|
|
237
|
-
'client/components/crm/shared/LeadsTable.tsx',
|
|
238
|
-
'client/components/crm/bizdev/dealPartners/DealPartnersTablePanel.tsx',
|
|
239
|
-
'client/pages/crm/bizdev/tabs/PartnersTab.tsx',
|
|
240
|
-
'client/pages/crm/bizdev/tabs/CsmsTab.tsx',
|
|
241
|
-
]) {
|
|
242
|
-
applyLiteralReplacement(path.join(CROSSPATH_ROOT, relativePath), (source) =>
|
|
243
|
-
source
|
|
244
|
-
.replaceAll('Array<Promise<unknown>>', 'Array<PromiseLike<unknown>>')
|
|
245
|
-
.replaceAll('Promise<void>[]', 'PromiseLike<void>[]'),
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const parseMissingContextNames = () => {
|
|
251
|
-
const files = new Map<string, Set<string>>();
|
|
252
|
-
const pattern =
|
|
253
|
-
/^(.+?)\(\d+,\d+\): error TS2304: Cannot find name '(Investor|Crm|Prospect|Headhunters|Founder|Domains|Navigation|Router|Users|Plans|Auth|Admin)'\.$/gm;
|
|
254
|
-
|
|
255
|
-
for (const log of LOGS) {
|
|
256
|
-
if (!fs.existsSync(log.filepath)) continue;
|
|
257
|
-
|
|
258
|
-
const content = fs.readFileSync(log.filepath, 'utf8');
|
|
259
|
-
let match: RegExpExecArray | null;
|
|
260
|
-
|
|
261
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
262
|
-
const relativePath = match[1];
|
|
263
|
-
const identifier = match[2];
|
|
264
|
-
const absolutePath = path.resolve(log.baseDir, relativePath);
|
|
265
|
-
|
|
266
|
-
if (!fs.existsSync(absolutePath) || absolutePath.includes('/.generated/')) continue;
|
|
267
|
-
|
|
268
|
-
let names = files.get(absolutePath);
|
|
269
|
-
if (!names) {
|
|
270
|
-
names = new Set<string>();
|
|
271
|
-
files.set(absolutePath, names);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
names.add(identifier);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return files;
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const findChildContainingPosition = (node: ts.Node, position: number): ts.Node | undefined => {
|
|
282
|
-
let matchedChild: ts.Node | undefined;
|
|
283
|
-
|
|
284
|
-
node.forEachChild((child) => {
|
|
285
|
-
if (position >= child.getFullStart() && position < child.getEnd()) matchedChild = child;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
return matchedChild;
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const findEnclosingFunction = (sourceFile: ts.SourceFile, position: number) => {
|
|
292
|
-
let current: ts.Node = sourceFile;
|
|
293
|
-
const functions: Array<
|
|
294
|
-
| ts.FunctionDeclaration
|
|
295
|
-
| ts.FunctionExpression
|
|
296
|
-
| ts.ArrowFunction
|
|
297
|
-
| ts.MethodDeclaration
|
|
298
|
-
| ts.GetAccessorDeclaration
|
|
299
|
-
| ts.SetAccessorDeclaration
|
|
300
|
-
| ts.ConstructorDeclaration
|
|
301
|
-
> = [];
|
|
302
|
-
|
|
303
|
-
while (true) {
|
|
304
|
-
const next = findChildContainingPosition(current, position);
|
|
305
|
-
if (!next) return functions[0];
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
ts.isFunctionDeclaration(next) ||
|
|
309
|
-
ts.isFunctionExpression(next) ||
|
|
310
|
-
ts.isArrowFunction(next) ||
|
|
311
|
-
ts.isMethodDeclaration(next) ||
|
|
312
|
-
ts.isGetAccessorDeclaration(next) ||
|
|
313
|
-
ts.isSetAccessorDeclaration(next) ||
|
|
314
|
-
ts.isConstructorDeclaration(next)
|
|
315
|
-
) {
|
|
316
|
-
functions.push(next);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
current = next;
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
const getContextHookName = (sourceFile: ts.SourceFile) => {
|
|
324
|
-
for (const statement of sourceFile.statements) {
|
|
325
|
-
if (!ts.isImportDeclaration(statement)) continue;
|
|
326
|
-
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
327
|
-
if (statement.moduleSpecifier.text !== '@/client/context') continue;
|
|
328
|
-
if (!statement.importClause?.name) continue;
|
|
329
|
-
|
|
330
|
-
return statement.importClause.name.text;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return null;
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const ensureContextImport = (filepath: string, source: string, sourceFile: ts.SourceFile) => {
|
|
337
|
-
const existing = getContextHookName(sourceFile);
|
|
338
|
-
if (existing) return { source, hookName: existing };
|
|
339
|
-
|
|
340
|
-
const hookName = 'useAppContext';
|
|
341
|
-
const importStatement = `import ${hookName} from "@/client/context";\n`;
|
|
342
|
-
|
|
343
|
-
let insertPos = 0;
|
|
344
|
-
for (const statement of sourceFile.statements) {
|
|
345
|
-
if (!ts.isImportDeclaration(statement)) break;
|
|
346
|
-
insertPos = statement.getEnd() + 1;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const nextSource = source.slice(0, insertPos) + importStatement + source.slice(insertPos);
|
|
350
|
-
console.info(`added context import in ${filepath}`);
|
|
351
|
-
|
|
352
|
-
return { source: nextSource, hookName };
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const findExistingContextBinding = (
|
|
356
|
-
functionBody: ts.Block,
|
|
357
|
-
hookName: string,
|
|
358
|
-
): ts.VariableDeclaration | undefined => {
|
|
359
|
-
for (const statement of functionBody.statements) {
|
|
360
|
-
if (!ts.isVariableStatement(statement)) continue;
|
|
361
|
-
|
|
362
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
363
|
-
if (!ts.isObjectBindingPattern(declaration.name)) continue;
|
|
364
|
-
if (!declaration.initializer || !ts.isCallExpression(declaration.initializer)) continue;
|
|
365
|
-
if (!ts.isIdentifier(declaration.initializer.expression) || declaration.initializer.expression.text !== hookName)
|
|
366
|
-
continue;
|
|
367
|
-
|
|
368
|
-
return declaration;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return undefined;
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const isContextBindingDeclaration = (declaration: ts.VariableDeclaration, hookName: string) => {
|
|
376
|
-
if (!ts.isObjectBindingPattern(declaration.name)) return false;
|
|
377
|
-
if (!declaration.initializer || !ts.isCallExpression(declaration.initializer)) return false;
|
|
378
|
-
if (!ts.isIdentifier(declaration.initializer.expression) || declaration.initializer.expression.text !== hookName)
|
|
379
|
-
return false;
|
|
380
|
-
|
|
381
|
-
return declaration.name.elements.every((element) => {
|
|
382
|
-
const identifier = element.propertyName || element.name;
|
|
383
|
-
return ts.isIdentifier(identifier) && TARGET_CONTEXT_NAMES.has(identifier.text);
|
|
384
|
-
});
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const removeNestedContextBindings = (source: string, parentFunction: ts.Node, hookName: string) => {
|
|
388
|
-
const removals: Array<{ start: number; end: number }> = [];
|
|
389
|
-
|
|
390
|
-
const visit = (node: ts.Node) => {
|
|
391
|
-
if (node !== parentFunction && ts.isBlock(node) && node.parent) {
|
|
392
|
-
const functionParent = node.parent;
|
|
393
|
-
if (
|
|
394
|
-
ts.isFunctionDeclaration(functionParent) ||
|
|
395
|
-
ts.isFunctionExpression(functionParent) ||
|
|
396
|
-
ts.isArrowFunction(functionParent) ||
|
|
397
|
-
ts.isMethodDeclaration(functionParent) ||
|
|
398
|
-
ts.isGetAccessorDeclaration(functionParent) ||
|
|
399
|
-
ts.isSetAccessorDeclaration(functionParent) ||
|
|
400
|
-
ts.isConstructorDeclaration(functionParent)
|
|
401
|
-
) {
|
|
402
|
-
for (const statement of node.statements) {
|
|
403
|
-
if (!ts.isVariableStatement(statement)) continue;
|
|
404
|
-
|
|
405
|
-
const allContextBindings =
|
|
406
|
-
statement.declarationList.declarations.length > 0 &&
|
|
407
|
-
statement.declarationList.declarations.every((declaration) =>
|
|
408
|
-
isContextBindingDeclaration(declaration, hookName),
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
if (allContextBindings) removals.push({ start: statement.getFullStart(), end: statement.getEnd() });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
node.forEachChild(visit);
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
parentFunction.forEachChild(visit);
|
|
420
|
-
|
|
421
|
-
return removals
|
|
422
|
-
.sort((left, right) => right.start - left.start)
|
|
423
|
-
.reduce((nextSource, removal) => nextSource.slice(0, removal.start) + nextSource.slice(removal.end), source);
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
const addContextBindings = (filepath: string, names: Set<string>) => {
|
|
427
|
-
let source = fs.readFileSync(filepath, 'utf8');
|
|
428
|
-
let sourceFile = ts.createSourceFile(filepath, source, ts.ScriptTarget.Latest, true, filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
429
|
-
const importResult = ensureContextImport(filepath, source, sourceFile);
|
|
430
|
-
source = importResult.source;
|
|
431
|
-
sourceFile = ts.createSourceFile(filepath, source, ts.ScriptTarget.Latest, true, filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
432
|
-
const hookName = importResult.hookName;
|
|
433
|
-
|
|
434
|
-
const positions = [...names]
|
|
435
|
-
.map((name) => source.indexOf(`${name}.`))
|
|
436
|
-
.filter((position) => position >= 0)
|
|
437
|
-
.sort((left, right) => left - right);
|
|
438
|
-
|
|
439
|
-
if (positions.length === 0) return;
|
|
440
|
-
|
|
441
|
-
const targetFunction = findEnclosingFunction(sourceFile, positions[0]);
|
|
442
|
-
if (!targetFunction || !targetFunction.body || !ts.isBlock(targetFunction.body)) {
|
|
443
|
-
console.warn(`could not find function body for ${filepath}`);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
source = removeNestedContextBindings(source, targetFunction, hookName);
|
|
448
|
-
sourceFile = ts.createSourceFile(filepath, source, ts.ScriptTarget.Latest, true, filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
449
|
-
const refreshedTargetFunction = findEnclosingFunction(sourceFile, positions[0]);
|
|
450
|
-
if (!refreshedTargetFunction || !refreshedTargetFunction.body || !ts.isBlock(refreshedTargetFunction.body)) {
|
|
451
|
-
console.warn(`could not refresh function body for ${filepath}`);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const existingBinding = findExistingContextBinding(refreshedTargetFunction.body, hookName);
|
|
456
|
-
|
|
457
|
-
if (existingBinding && ts.isObjectBindingPattern(existingBinding.name)) {
|
|
458
|
-
const currentElements = existingBinding.name.elements.map((element) => element.getText(sourceFile));
|
|
459
|
-
const mergedElements = [...currentElements];
|
|
460
|
-
|
|
461
|
-
for (const name of names) {
|
|
462
|
-
if (!TARGET_CONTEXT_NAMES.has(name)) continue;
|
|
463
|
-
if (currentElements.some((element) => element === name || element.startsWith(name + ':'))) continue;
|
|
464
|
-
mergedElements.push(name);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const start = existingBinding.name.getStart(sourceFile) + 1;
|
|
468
|
-
const end = existingBinding.name.getEnd() - 1;
|
|
469
|
-
source = source.slice(0, start) + ` ${mergedElements.join(', ')} ` + source.slice(end);
|
|
470
|
-
} else {
|
|
471
|
-
const functionLineStart = source.lastIndexOf('\n', refreshedTargetFunction.getStart(sourceFile) - 1) + 1;
|
|
472
|
-
const functionIndent =
|
|
473
|
-
source.slice(functionLineStart, refreshedTargetFunction.getStart(sourceFile)).match(/^\s*/)?.[0] || '';
|
|
474
|
-
const statementIndent = functionIndent + ' ';
|
|
475
|
-
const insertPos = refreshedTargetFunction.body.getStart(sourceFile) + 1;
|
|
476
|
-
const statement = `\n${statementIndent}const { ${[...names].join(', ')} } = ${hookName}();`;
|
|
477
|
-
source = source.slice(0, insertPos) + statement + source.slice(insertPos);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
writeIfChanged(filepath, source);
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
const patchAmbientContextUsages = () => {
|
|
484
|
-
const files = parseMissingContextNames();
|
|
485
|
-
|
|
486
|
-
for (const [filepath, names] of files) addContextBindings(filepath, names);
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
patchCentralFiles();
|
|
490
|
-
patchAmbientContextUsages();
|
|
@@ -1,119 +0,0 @@
|
|
|
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
|
-
});
|