@genn-inc/cluebase-cli 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/README.md +101 -0
- package/bin/cluebase-cli.mjs +11 -0
- package/package.json +17 -0
- package/src/cli-command.mjs +515 -0
- package/src/cli-invocation.mjs +17 -0
- package/src/code-evidence-analyzer.mjs +2041 -0
- package/src/contracts.mjs +36 -0
- package/src/generated-code-evidence-contract.mjs +22 -0
- package/src/generated-sdk-version-contract.mjs +5 -0
- package/src/generated-source-path-policy.mjs +20 -0
- package/src/lifecycle-guard.mjs +202 -0
- package/src/path-policy.mjs +81 -0
- package/src/setup-ai-contract.mjs +221 -0
- package/src/setup-check-constants.mjs +110 -0
- package/src/setup-check-scan-a.mjs +849 -0
- package/src/setup-check-scan-b.mjs +994 -0
- package/src/setup-check.mjs +575 -0
- package/src/setup-discover-check.mjs +755 -0
- package/src/setup-doctor-deadline.mjs +221 -0
- package/src/setup-doctor-env.mjs +331 -0
- package/src/setup-doctor-file-boundary.mjs +426 -0
- package/src/setup-doctor-probe.mjs +719 -0
- package/src/setup-doctor-quality-checks-a.mjs +593 -0
- package/src/setup-doctor-quality-checks-b.mjs +638 -0
- package/src/setup-doctor-quality-shared.mjs +382 -0
- package/src/setup-doctor-quality.mjs +209 -0
- package/src/setup-doctor-route-scan.mjs +160 -0
- package/src/setup-doctor-sdk-probe.mjs +340 -0
- package/src/setup-doctor.mjs +545 -0
- package/src/setup-documents.mjs +112 -0
- package/src/setup-help.mjs +130 -0
- package/src/setup-prepare.mjs +360 -0
- package/src/setup-repository-discovery.mjs +764 -0
- package/src/setup-step-builders-discover.mjs +701 -0
- package/src/setup-step-builders-events.mjs +229 -0
- package/src/setup-step-builders-implement.mjs +710 -0
- package/src/setup-step-commands.mjs +427 -0
- package/src/setup-tool.mjs +27 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
// setup-check lifecycle/find detectors (part 2). Extracted for file-size
|
|
2
|
+
// limits; content is unchanged.
|
|
3
|
+
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
stripSourceNoise,
|
|
7
|
+
} from "./lifecycle-guard.mjs";
|
|
8
|
+
import {
|
|
9
|
+
escapeRegex,
|
|
10
|
+
nextPackageRoots,
|
|
11
|
+
sourceIsUnderAnyRoot,
|
|
12
|
+
hasUseClientDirective,
|
|
13
|
+
sourceImportsFrontendSdk,
|
|
14
|
+
} from "./setup-check-scan-a.mjs";
|
|
15
|
+
|
|
16
|
+
export const findNextLifecycleMissingUseClientFiles = ({
|
|
17
|
+
dependencySources,
|
|
18
|
+
frontendSources,
|
|
19
|
+
}) => {
|
|
20
|
+
const roots = nextPackageRoots(dependencySources);
|
|
21
|
+
if (roots.length === 0) return [];
|
|
22
|
+
return frontendSources
|
|
23
|
+
.filter((source) => sourceIsUnderAnyRoot(source, roots))
|
|
24
|
+
.filter(
|
|
25
|
+
(source) =>
|
|
26
|
+
sourceImportsFrontendSdk(source) ||
|
|
27
|
+
/\bcluebase\.init\s*\(/.test(
|
|
28
|
+
stripSourceNoise(source.text, { stripStrings: true }),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
.filter((source) => !hasUseClientDirective(source.text))
|
|
32
|
+
.map((source) => source.file_path);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const findCluebaseInitCallEmptyEnvFallbackFiles = (frontendSources) =>
|
|
36
|
+
frontendSources
|
|
37
|
+
.filter((source) => sourceImportsFrontendSdk(source))
|
|
38
|
+
.filter((source) =>
|
|
39
|
+
/(?:process\.env\.(?:NEXT_PUBLIC_|PUBLIC_|NUXT_PUBLIC_|REACT_APP_)?CLUEBASE_[A-Z0-9_]+|import\.meta\.env\.VITE_CLUEBASE_[A-Z0-9_]+)\s*(?:\?\?|\|\|)\s*(["'])\1/.test(
|
|
40
|
+
stripSourceNoise(source.text),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
.map((source) => source.file_path);
|
|
44
|
+
|
|
45
|
+
export const findFrontendInitBeforeCluebaseInitCallFiles = (frontendSources) =>
|
|
46
|
+
frontendSources
|
|
47
|
+
.filter((source) => sourceImportsFrontendSdk(source))
|
|
48
|
+
.filter((source) => {
|
|
49
|
+
const text = stripSourceNoise(source.text, { stripStrings: true });
|
|
50
|
+
const initializedIndex = text.search(/\binitialized\s*=\s*true\b/);
|
|
51
|
+
const cluebaseInitIndex = text.search(/\bcluebase\.init\s*\(/);
|
|
52
|
+
return (
|
|
53
|
+
initializedIndex >= 0 &&
|
|
54
|
+
cluebaseInitIndex >= 0 &&
|
|
55
|
+
initializedIndex < cluebaseInitIndex
|
|
56
|
+
);
|
|
57
|
+
})
|
|
58
|
+
.map((source) => source.file_path);
|
|
59
|
+
|
|
60
|
+
export const findFrontendBackendTracePropagationMissingFiles = ({
|
|
61
|
+
backendPresent,
|
|
62
|
+
frontendSources,
|
|
63
|
+
}) => {
|
|
64
|
+
if (!backendPresent) return [];
|
|
65
|
+
return frontendSources
|
|
66
|
+
.filter((source) =>
|
|
67
|
+
/\bcluebase\.init\s*\(/.test(
|
|
68
|
+
stripSourceNoise(source.text, { stripStrings: true }),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
.filter(
|
|
72
|
+
(source) =>
|
|
73
|
+
!/\btracePropagationOrigins\b/.test(
|
|
74
|
+
stripSourceNoise(source.text, { stripStrings: true }),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
.map((source) => source.file_path);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const nextRouteApiPathFromFile = (source) => {
|
|
81
|
+
const normalized = source.file_path.replaceAll("\\", "/");
|
|
82
|
+
const match = /(?:^|\/)(?:src\/)?app\/(.+)\/route\.(?:ts|tsx|js|jsx)$/.exec(
|
|
83
|
+
normalized,
|
|
84
|
+
);
|
|
85
|
+
if (!match) return null;
|
|
86
|
+
const routeSegments = match[1]
|
|
87
|
+
.split("/")
|
|
88
|
+
.filter((segment) => segment && !/^\(.+\)$/.test(segment));
|
|
89
|
+
if (routeSegments[0] !== "api") return null;
|
|
90
|
+
return `/${routeSegments.join("/")}`.replace(/\/+/g, "/").toLowerCase();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const findBackendSdkInitMissingServiceKeyFiles = ({
|
|
94
|
+
backendSources,
|
|
95
|
+
framework,
|
|
96
|
+
}) => {
|
|
97
|
+
if (framework !== "fastapi") return [];
|
|
98
|
+
return backendSources
|
|
99
|
+
.filter((source) =>
|
|
100
|
+
/\b(?:cluebase\.init|cluebase_init_fastapi)\s*\(/.test(
|
|
101
|
+
stripSourceNoise(source.text, { stripStrings: true }),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
.filter(
|
|
105
|
+
(source) =>
|
|
106
|
+
!/\bcluebase\.init\s*\(\s*\{[\s\S]{0,1600}["']service_key["']\s*:/.test(
|
|
107
|
+
stripSourceNoise(source.text),
|
|
108
|
+
) &&
|
|
109
|
+
!/\bcluebase_init_fastapi\s*\([\s\S]{0,1600}\bservice_key\s*=/.test(
|
|
110
|
+
stripSourceNoise(source.text),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
.map((source) => source.file_path);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const findFastapiMissingMiddlewareFiles = ({ backendSources, framework }) => {
|
|
117
|
+
if (framework !== "fastapi") return [];
|
|
118
|
+
const combined = backendSources
|
|
119
|
+
.map((source) => stripSourceNoise(source.text, { stripStrings: true }))
|
|
120
|
+
.join("\n");
|
|
121
|
+
if (
|
|
122
|
+
/\bcluebase_init_fastapi\s*\(/.test(combined) ||
|
|
123
|
+
/\bCluebaseFastApiMiddleware\b/.test(combined)
|
|
124
|
+
) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return backendSources
|
|
128
|
+
.filter((source) =>
|
|
129
|
+
/\bFastAPI\s*\(/.test(
|
|
130
|
+
stripSourceNoise(source.text, { stripStrings: true }),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
.map((source) => source.file_path);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const findBackendCluebaseEnvRequiredIndexFiles = (backendSources) =>
|
|
137
|
+
backendSources
|
|
138
|
+
.filter((source) =>
|
|
139
|
+
/os\.environ\s*\[\s*["']CLUEBASE_[A-Z0-9_]+["']\s*\]/.test(
|
|
140
|
+
stripSourceNoise(source.text),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
.map((source) => source.file_path);
|
|
144
|
+
|
|
145
|
+
export const executableLifecycleCallSnippets = (text) => {
|
|
146
|
+
const snippets = [];
|
|
147
|
+
let index = 0;
|
|
148
|
+
while (index < text.length) {
|
|
149
|
+
const char = text[index];
|
|
150
|
+
const next = text[index + 1];
|
|
151
|
+
if (char === "/" && next === "/") {
|
|
152
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (char === "#") {
|
|
156
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === "/" && next === "*") {
|
|
160
|
+
index += 2;
|
|
161
|
+
while (
|
|
162
|
+
index < text.length &&
|
|
163
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
164
|
+
) {
|
|
165
|
+
index += 1;
|
|
166
|
+
}
|
|
167
|
+
index += index < text.length ? 2 : 0;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
171
|
+
const quote = char;
|
|
172
|
+
index += 1;
|
|
173
|
+
while (index < text.length) {
|
|
174
|
+
if (quote !== "`" && text[index] === "\\") {
|
|
175
|
+
index += 2;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (text[index] === quote) {
|
|
179
|
+
index += 1;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
index += 1;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const match = /^(cluebase\.identify|cluebase\.group)\b/.exec(text.slice(index));
|
|
187
|
+
if (!match || /[A-Za-z0-9_$]/.test(text[index - 1] ?? "")) {
|
|
188
|
+
index += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let cursor = index + match[1].length;
|
|
192
|
+
while (/\s/.test(text[cursor] ?? "")) cursor += 1;
|
|
193
|
+
if (text[cursor] !== "(") {
|
|
194
|
+
index += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
let depth = 0;
|
|
198
|
+
let end = cursor;
|
|
199
|
+
while (end < text.length) {
|
|
200
|
+
const current = text[end];
|
|
201
|
+
if (current === "'" || current === '"' || current === "`") {
|
|
202
|
+
const quote = current;
|
|
203
|
+
end += 1;
|
|
204
|
+
while (end < text.length) {
|
|
205
|
+
if (quote !== "`" && text[end] === "\\") {
|
|
206
|
+
end += 2;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (text[end] === quote) {
|
|
210
|
+
end += 1;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
end += 1;
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (current === "(") depth += 1;
|
|
218
|
+
if (current === ")") {
|
|
219
|
+
depth -= 1;
|
|
220
|
+
if (depth === 0) {
|
|
221
|
+
end += 1;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
end += 1;
|
|
226
|
+
}
|
|
227
|
+
snippets.push(text.slice(index, end));
|
|
228
|
+
index = end;
|
|
229
|
+
}
|
|
230
|
+
return snippets;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const sourceHasHardcodedLifecycleIdentity = (source) => {
|
|
234
|
+
const text = stripSourceNoise(source.text);
|
|
235
|
+
const lifecycleCalls = executableLifecycleCallSnippets(source.text);
|
|
236
|
+
// cluebase.identify(<arg1>): arg1 が hardcoded string なら NG
|
|
237
|
+
// cluebase.group(<arg1>, <arg2>): arg1 は groupType literal で OK、 arg2 が hardcoded string なら NG
|
|
238
|
+
if (
|
|
239
|
+
lifecycleCalls.some((call) =>
|
|
240
|
+
/\bcluebase\.identify\s*\(\s*(?:["'][^"']+["']|`[^`$]+`)/.test(call),
|
|
241
|
+
)
|
|
242
|
+
) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
lifecycleCalls.some((call) =>
|
|
247
|
+
// cluebase.group の第 2 引数 (= groupKey) が hardcoded string なら NG
|
|
248
|
+
// 第 1 引数 (= type literal "organization") は spec 通り
|
|
249
|
+
/\bcluebase\.group\s*\(\s*(?:["'][^"']+["']|`[^`$]+`)\s*,\s*(?:["'][^"']+["']|`[^`$]+`)/.test(
|
|
250
|
+
call,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
lifecycleCalls.some((call) =>
|
|
258
|
+
/\b(?:cluebase\.identify|cluebase\.group)\s*\(\s*\{[\s\S]{0,500}\b(?:id|userId|user_id|accountId|account_id|workspaceId|workspace_id)\b\s*:\s*(?:["'][^"']+["']|`[^`$]+`)/.test(
|
|
259
|
+
call,
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
const hardcodedIdentifiers = [
|
|
266
|
+
...text.matchAll(
|
|
267
|
+
/(?:^|[;\n])\s*(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*(?:["'][^"']+["']|`[^`$]+`)/g,
|
|
268
|
+
),
|
|
269
|
+
].map((match) => match[1]);
|
|
270
|
+
if (
|
|
271
|
+
hardcodedIdentifiers.some((name) =>
|
|
272
|
+
lifecycleCalls.some((call) =>
|
|
273
|
+
// identify は第 1 引数、 group は第 2 引数 (= groupKey) が対象
|
|
274
|
+
new RegExp(
|
|
275
|
+
`\\b(?:cluebase\\.identify\\s*\\(\\s*${name}\\b|cluebase\\.group\\s*\\(\\s*(?:["'][^"']+["']|\`[^\`$]+\`)\\s*,\\s*${name}\\b)`,
|
|
276
|
+
).test(call),
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
const hardcodedObjectNames = [
|
|
283
|
+
...text.matchAll(
|
|
284
|
+
/(?:^|[;\n])\s*(?:(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*\{[\s\S]{0,800}:\s*["'][^"']+["'][\s\S]{0,800}\}/g,
|
|
285
|
+
),
|
|
286
|
+
].map((match) => match[1]);
|
|
287
|
+
return hardcodedObjectNames.some((name) =>
|
|
288
|
+
lifecycleCalls.some((call) =>
|
|
289
|
+
new RegExp(
|
|
290
|
+
`\\b(?:cluebase\\.identify\\s*\\(\\s*${name}\\s*(?:\\.|\\[)|cluebase\\.group\\s*\\(\\s*(?:["'][^"']+["']|\`[^\`$]+\`)\\s*,\\s*${name}\\s*(?:\\.|\\[))`,
|
|
291
|
+
).test(call),
|
|
292
|
+
),
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export const findHardcodedLifecycleIdentityFiles = (sources) =>
|
|
297
|
+
sources
|
|
298
|
+
.filter(sourceHasHardcodedLifecycleIdentity)
|
|
299
|
+
.map((source) => source.file_path);
|
|
300
|
+
|
|
301
|
+
export const READ_SIDE_EFFECT_LIFECYCLE_PATTERN =
|
|
302
|
+
/\b(?:cluebase\.init|cluebase\.identify|cluebase\.group|cluebase\.reset)\s*\(/;
|
|
303
|
+
|
|
304
|
+
export const frontendQueryFunctionBlocks = (text) => {
|
|
305
|
+
const stripped = stripSourceNoise(text, { stripStrings: true });
|
|
306
|
+
const lines = stripped.split(/\r?\n/);
|
|
307
|
+
const blocks = [];
|
|
308
|
+
for (const match of stripped.matchAll(
|
|
309
|
+
/\b(?:useQuery|useSuspenseQuery|useInfiniteQuery|queryOptions)\s*\(/g,
|
|
310
|
+
)) {
|
|
311
|
+
blocks.push(
|
|
312
|
+
balancedBlockFrom({
|
|
313
|
+
close: ")",
|
|
314
|
+
open: "(",
|
|
315
|
+
startIndex: match.index,
|
|
316
|
+
text: stripped,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
321
|
+
const line = lines[index];
|
|
322
|
+
if (!/\bqueryFn\b/.test(line)) continue;
|
|
323
|
+
const block = [line];
|
|
324
|
+
if (line.includes("{")) {
|
|
325
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
326
|
+
block.push(lines[cursor]);
|
|
327
|
+
if (/^\s*}\s*,?\s*$/.test(lines[cursor])) break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
blocks.push(block.join("\n"));
|
|
331
|
+
}
|
|
332
|
+
for (const match of stripped.matchAll(
|
|
333
|
+
/\bqueryFn\s*(?::\s*([A-Za-z_$][\w$]*)\b)?/g,
|
|
334
|
+
)) {
|
|
335
|
+
const name = match[1] ?? "queryFn";
|
|
336
|
+
const declaration = new RegExp(
|
|
337
|
+
`(?:(?:export\\s+)?function\\s+${escapeRegex(name)}\\s*\\([^)]*\\)|(?:export\\s+)?(?:const|let|var)\\s+${escapeRegex(name)}\\s*=\\s*(?:async\\s*)?(?:\\([^)]*\\)|[A-Za-z_$][\\w$]*)\\s*=>)[\\s\\S]{0,1600}`,
|
|
338
|
+
"g",
|
|
339
|
+
);
|
|
340
|
+
blocks.push(
|
|
341
|
+
...[...stripped.matchAll(declaration)].map((entry) => entry[0]),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return blocks;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export const frontendLifecycleExportedFunctionNames = (source) => {
|
|
348
|
+
const stripped = stripSourceNoise(source.text, { stripStrings: true });
|
|
349
|
+
const names = [];
|
|
350
|
+
const declarations = [
|
|
351
|
+
...stripped.matchAll(
|
|
352
|
+
/\bexport\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{[\s\S]{0,1600}/g,
|
|
353
|
+
),
|
|
354
|
+
...stripped.matchAll(
|
|
355
|
+
/\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>[\s\S]{0,1600}/g,
|
|
356
|
+
),
|
|
357
|
+
];
|
|
358
|
+
for (const declaration of declarations) {
|
|
359
|
+
if (READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(declaration[0])) {
|
|
360
|
+
names.push(declaration[1]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return names;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
export const sourceHasDefaultLifecycleExport = (source) => {
|
|
367
|
+
const stripped = stripSourceNoise(source.text, { stripStrings: true });
|
|
368
|
+
for (const match of stripped.matchAll(
|
|
369
|
+
/\bexport\s+default\s+(?:async\s+)?function\b[^{]*\{/g,
|
|
370
|
+
)) {
|
|
371
|
+
if (
|
|
372
|
+
READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(
|
|
373
|
+
balancedBlockFrom({
|
|
374
|
+
close: "}",
|
|
375
|
+
open: "{",
|
|
376
|
+
startIndex: match.index,
|
|
377
|
+
text: stripped,
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
380
|
+
) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const match of stripped.matchAll(
|
|
385
|
+
/\bexport\s+default\s+(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/g,
|
|
386
|
+
)) {
|
|
387
|
+
if (
|
|
388
|
+
READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(
|
|
389
|
+
balancedBlockFrom({
|
|
390
|
+
close: "}",
|
|
391
|
+
open: "{",
|
|
392
|
+
startIndex: match.index,
|
|
393
|
+
text: stripped,
|
|
394
|
+
}),
|
|
395
|
+
)
|
|
396
|
+
) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const match of stripped.matchAll(
|
|
401
|
+
/\bexport\s+default\s+([A-Za-z_$][\w$]*)\b/g,
|
|
402
|
+
)) {
|
|
403
|
+
const name = match[1];
|
|
404
|
+
const declaration = new RegExp(
|
|
405
|
+
`(?:(?:export\\s+)?function\\s+${escapeRegex(name)}\\s*\\([^)]*\\)\\s*\\{|(?:export\\s+)?(?:const|let|var)\\s+${escapeRegex(name)}\\s*=\\s*(?:async\\s*)?(?:\\([^)]*\\)|[A-Za-z_$][\\w$]*)\\s*=>\\s*\\{)`,
|
|
406
|
+
"g",
|
|
407
|
+
);
|
|
408
|
+
for (const declarationMatch of stripped.matchAll(declaration)) {
|
|
409
|
+
if (
|
|
410
|
+
READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(
|
|
411
|
+
balancedBlockFrom({
|
|
412
|
+
close: "}",
|
|
413
|
+
open: "{",
|
|
414
|
+
startIndex: declarationMatch.index,
|
|
415
|
+
text: stripped,
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
export const frontendImportCandidates = ({ importerPath, specifier }) => {
|
|
427
|
+
let base = null;
|
|
428
|
+
if (specifier.startsWith(".")) {
|
|
429
|
+
base = join(
|
|
430
|
+
dirname(importerPath),
|
|
431
|
+
specifier.startsWith("./") || specifier.startsWith("../")
|
|
432
|
+
? specifier
|
|
433
|
+
: specifier.replace(/^\.+/, ""),
|
|
434
|
+
).replaceAll("\\", "/");
|
|
435
|
+
} else if (specifier.startsWith("@/") || specifier.startsWith("~/")) {
|
|
436
|
+
const pathParts = importerPath.replaceAll("\\", "/").split("/");
|
|
437
|
+
const srcIndex = pathParts.lastIndexOf("src");
|
|
438
|
+
const aliasBase =
|
|
439
|
+
srcIndex >= 0
|
|
440
|
+
? pathParts.slice(0, srcIndex + 1).join("/")
|
|
441
|
+
: pathParts.slice(0, 1).join("/");
|
|
442
|
+
base = join(aliasBase, specifier.slice(2)).replaceAll("\\", "/");
|
|
443
|
+
}
|
|
444
|
+
if (!base) return [];
|
|
445
|
+
return [
|
|
446
|
+
base,
|
|
447
|
+
`${base}.ts`,
|
|
448
|
+
`${base}.tsx`,
|
|
449
|
+
`${base}.js`,
|
|
450
|
+
`${base}.jsx`,
|
|
451
|
+
join(base, "index.ts").replaceAll("\\", "/"),
|
|
452
|
+
join(base, "index.tsx").replaceAll("\\", "/"),
|
|
453
|
+
join(base, "index.js").replaceAll("\\", "/"),
|
|
454
|
+
join(base, "index.jsx").replaceAll("\\", "/"),
|
|
455
|
+
];
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
export const frontendQueryFnLifecycleAliasNames = ({
|
|
459
|
+
frontendSources,
|
|
460
|
+
querySource,
|
|
461
|
+
}) => {
|
|
462
|
+
const sourceByPath = new Map(
|
|
463
|
+
frontendSources.map((source) => [source.file_path, source]),
|
|
464
|
+
);
|
|
465
|
+
const aliases = new Set();
|
|
466
|
+
const text = stripSourceNoise(querySource.text);
|
|
467
|
+
for (const match of text.matchAll(
|
|
468
|
+
/\bimport\s+([^;]+?)\s+from\s+["']([^"']+)["']/g,
|
|
469
|
+
)) {
|
|
470
|
+
const clause = match[1].trim();
|
|
471
|
+
const importedSource = frontendImportCandidates({
|
|
472
|
+
importerPath: querySource.file_path,
|
|
473
|
+
specifier: match[2],
|
|
474
|
+
})
|
|
475
|
+
.map((candidate) => sourceByPath.get(candidate))
|
|
476
|
+
.find(Boolean);
|
|
477
|
+
if (!importedSource) continue;
|
|
478
|
+
const exportedLifecycleNames = new Set(
|
|
479
|
+
frontendLifecycleExportedFunctionNames(importedSource),
|
|
480
|
+
);
|
|
481
|
+
const namedImport = /\{([\s\S]*)\}/.exec(clause);
|
|
482
|
+
if (namedImport) {
|
|
483
|
+
for (const rawPart of namedImport[1].split(",")) {
|
|
484
|
+
const part = rawPart.trim();
|
|
485
|
+
if (!part) continue;
|
|
486
|
+
const [exported, local = exported] = part
|
|
487
|
+
.split(/\s+as\s+/i)
|
|
488
|
+
.map((entry) => entry.trim());
|
|
489
|
+
if (exportedLifecycleNames.has(exported)) {
|
|
490
|
+
aliases.add(local);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const defaultImport = clause
|
|
495
|
+
.replace(/\{[\s\S]*\}/, "")
|
|
496
|
+
.split(",")[0]
|
|
497
|
+
.trim();
|
|
498
|
+
if (
|
|
499
|
+
defaultImport &&
|
|
500
|
+
/^[A-Za-z_$][\w$]*$/.test(defaultImport) &&
|
|
501
|
+
sourceHasDefaultLifecycleExport(importedSource)
|
|
502
|
+
) {
|
|
503
|
+
aliases.add(defaultImport);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return aliases;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export const findFrontendLifecycleReadSideEffectFiles = (frontendSources) => {
|
|
510
|
+
return frontendSources
|
|
511
|
+
.filter((source) => {
|
|
512
|
+
const text = stripSourceNoise(source.text, { stripStrings: true });
|
|
513
|
+
const hasQueryReadPath =
|
|
514
|
+
/\b(?:useQuery|useSuspenseQuery|useInfiniteQuery|queryOptions)\s*\(/.test(
|
|
515
|
+
text,
|
|
516
|
+
) || /\bqueryFn\b/.test(text);
|
|
517
|
+
if (!hasQueryReadPath) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
frontendQueryFunctionBlocks(source.text).some((block) =>
|
|
522
|
+
READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(block),
|
|
523
|
+
)
|
|
524
|
+
) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
const sameFileLifecycleHelpers = lifecycleHelperNamesInSource(source);
|
|
528
|
+
const queryFnBlocks = frontendQueryFunctionBlocks(source.text);
|
|
529
|
+
if (
|
|
530
|
+
queryFnBlocks.some((block) =>
|
|
531
|
+
blockCallsLifecycleHelper(block, sameFileLifecycleHelpers),
|
|
532
|
+
)
|
|
533
|
+
) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
const lifecycleAliases = frontendQueryFnLifecycleAliasNames({
|
|
537
|
+
frontendSources,
|
|
538
|
+
querySource: source,
|
|
539
|
+
});
|
|
540
|
+
return [...lifecycleAliases].some(
|
|
541
|
+
(alias) =>
|
|
542
|
+
[
|
|
543
|
+
new RegExp(`\\bqueryFn\\s*:\\s*${escapeRegex(alias)}\\b`),
|
|
544
|
+
...(alias === "queryFn" ? [/\bqueryFn\b/] : []),
|
|
545
|
+
new RegExp(
|
|
546
|
+
`\\bqueryFn\\s*:[\\s\\S]{0,600}\\b${escapeRegex(alias)}\\s*\\(`,
|
|
547
|
+
),
|
|
548
|
+
].some((pattern) => pattern.test(text)) ||
|
|
549
|
+
queryFnBlocks.some((block) =>
|
|
550
|
+
new RegExp(`\\b${escapeRegex(alias)}\\b`).test(block),
|
|
551
|
+
),
|
|
552
|
+
);
|
|
553
|
+
})
|
|
554
|
+
.map((source) => source.file_path);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export const REPEATED_IDENTITY_HELPER_PATTERN =
|
|
558
|
+
/\b(?:\w*refresh\w*|tokenRefresh|refresh_token|\w*current\w*|current_user|use(?:Current|User|Session)\w*|get(?:Current|Me|User|Session)\w*|fetch(?:Current|Me|User|Session)\w*|load(?:Current|Me|User|Session)\w*|\w*poll\w*|sessionPolling|requestExchange|queryFn|readHook)\b|\/me\b|current-user/i;
|
|
559
|
+
|
|
560
|
+
export const repeatedIdentityHelperContext = ({ matchIndex, text }) => {
|
|
561
|
+
const before = text.slice(0, matchIndex);
|
|
562
|
+
const starts = [
|
|
563
|
+
...before.matchAll(
|
|
564
|
+
/\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g,
|
|
565
|
+
),
|
|
566
|
+
...before.matchAll(
|
|
567
|
+
/\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/g,
|
|
568
|
+
),
|
|
569
|
+
...before.matchAll(
|
|
570
|
+
// Match `def` and `async def`, and stop at the opening paren so
|
|
571
|
+
// multi-line / return-annotated signatures still yield the enclosing
|
|
572
|
+
// function name. Without `async` support the enclosing `async def`
|
|
573
|
+
// handler is missed and the code falls back to a raw text window,
|
|
574
|
+
// which false-matches ORM calls like `db.refresh(user)`.
|
|
575
|
+
/^[ \t]*(?:async[ \t]+)?def\s+([A-Za-z_][\w]*)\s*\(/gm,
|
|
576
|
+
),
|
|
577
|
+
].sort((left, right) => left.index - right.index);
|
|
578
|
+
const start = starts.at(-1);
|
|
579
|
+
if (!start) {
|
|
580
|
+
return text.slice(Math.max(0, matchIndex - 220), matchIndex);
|
|
581
|
+
}
|
|
582
|
+
return start[1] ?? "";
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
export const findRepeatedIdentityHelperFiles = (sources) =>
|
|
586
|
+
sources
|
|
587
|
+
.filter((source) => {
|
|
588
|
+
const text = stripSourceNoise(source.text, { stripStrings: true });
|
|
589
|
+
for (const match of text.matchAll(/\bcluebase\.identify\s*\(/g)) {
|
|
590
|
+
const context = repeatedIdentityHelperContext({
|
|
591
|
+
matchIndex: match.index,
|
|
592
|
+
text,
|
|
593
|
+
});
|
|
594
|
+
if (REPEATED_IDENTITY_HELPER_PATTERN.test(context)) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return false;
|
|
599
|
+
})
|
|
600
|
+
.map((source) => source.file_path);
|
|
601
|
+
|
|
602
|
+
export const pythonGetEndpointBlocks = (text) => {
|
|
603
|
+
const lines = stripSourceNoise(text).split(/\r?\n/);
|
|
604
|
+
const blocks = [];
|
|
605
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
606
|
+
const line = lines[index];
|
|
607
|
+
if (!/^\s*@/.test(line)) continue;
|
|
608
|
+
const decoratorLines = [line];
|
|
609
|
+
let decoratorCursor = index + 1;
|
|
610
|
+
let parenBalance =
|
|
611
|
+
(line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
|
|
612
|
+
while (decoratorCursor < lines.length && parenBalance > 0) {
|
|
613
|
+
decoratorLines.push(lines[decoratorCursor]);
|
|
614
|
+
parenBalance +=
|
|
615
|
+
(lines[decoratorCursor].match(/\(/g) ?? []).length -
|
|
616
|
+
(lines[decoratorCursor].match(/\)/g) ?? []).length;
|
|
617
|
+
decoratorCursor += 1;
|
|
618
|
+
}
|
|
619
|
+
const decoratorText = decoratorLines.join("\n");
|
|
620
|
+
const isGetDecorator =
|
|
621
|
+
/^\s*@(app|router|[\w.]+)\.get\s*\(/.test(decoratorText) ||
|
|
622
|
+
(/^\s*@(app|router|[\w.]+)\.api_route\s*\(/.test(decoratorText) &&
|
|
623
|
+
/\bmethods\s*=\s*\[[\s\S]*GET[\s\S]*\]/i.test(decoratorText));
|
|
624
|
+
if (!isGetDecorator) continue;
|
|
625
|
+
const block = [...decoratorLines];
|
|
626
|
+
let cursor = decoratorCursor;
|
|
627
|
+
while (cursor < lines.length && /^\s*@/.test(lines[cursor])) {
|
|
628
|
+
block.push(lines[cursor]);
|
|
629
|
+
cursor += 1;
|
|
630
|
+
}
|
|
631
|
+
const defLine = lines[cursor] ?? "";
|
|
632
|
+
if (!/^\s*(?:async\s+)?def\s+/.test(defLine)) continue;
|
|
633
|
+
const defIndent = defLine.match(/^\s*/)?.[0].length ?? 0;
|
|
634
|
+
block.push(defLine);
|
|
635
|
+
cursor += 1;
|
|
636
|
+
while (cursor < lines.length) {
|
|
637
|
+
const current = lines[cursor];
|
|
638
|
+
const indent = current.match(/^\s*/)?.[0].length ?? 0;
|
|
639
|
+
if (current.trim() && indent <= defIndent) break;
|
|
640
|
+
block.push(current);
|
|
641
|
+
cursor += 1;
|
|
642
|
+
}
|
|
643
|
+
blocks.push(block.join("\n"));
|
|
644
|
+
}
|
|
645
|
+
return blocks;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
export const balancedBlockFrom = ({ close, open, startIndex, text }) => {
|
|
649
|
+
let depth = 0;
|
|
650
|
+
let started = false;
|
|
651
|
+
for (let index = startIndex; index < text.length; index += 1) {
|
|
652
|
+
const char = text[index];
|
|
653
|
+
if (char === open) {
|
|
654
|
+
depth += 1;
|
|
655
|
+
started = true;
|
|
656
|
+
} else if (char === close) {
|
|
657
|
+
depth -= 1;
|
|
658
|
+
if (started && depth === 0) {
|
|
659
|
+
return text.slice(startIndex, index + 1);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return text.slice(startIndex, Math.min(text.length, startIndex + 1600));
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
export const typescriptGetEndpointBlocks = (text) => {
|
|
667
|
+
const stripped = stripSourceNoise(text, { stripStrings: true });
|
|
668
|
+
const blocks = [
|
|
669
|
+
...stripped.matchAll(
|
|
670
|
+
/(?:^|\n)export\s+(?:async\s+)?function\s+GET\s*\([^)]*\)\s*(?::[^{]+)?\{/g,
|
|
671
|
+
),
|
|
672
|
+
].map((match) =>
|
|
673
|
+
balancedBlockFrom({
|
|
674
|
+
close: "}",
|
|
675
|
+
open: "{",
|
|
676
|
+
startIndex: match.index,
|
|
677
|
+
text: stripped,
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
680
|
+
for (const match of stripped.matchAll(
|
|
681
|
+
/(?:^|\n)export\s+const\s+GET\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)(?:\s*:[^=]+)?\s*=>\s*\{/g,
|
|
682
|
+
)) {
|
|
683
|
+
blocks.push(
|
|
684
|
+
balancedBlockFrom({
|
|
685
|
+
close: "}",
|
|
686
|
+
open: "{",
|
|
687
|
+
startIndex: match.index,
|
|
688
|
+
text: stripped,
|
|
689
|
+
}),
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
for (const match of stripped.matchAll(/(?:^|\n)\s*@Get\s*\([^)]*\)/g)) {
|
|
693
|
+
const nextDecoratorIndex = stripped
|
|
694
|
+
.slice(match.index + match[0].length)
|
|
695
|
+
.search(/\n\s*@[A-Za-z]/);
|
|
696
|
+
const sliceEnd =
|
|
697
|
+
nextDecoratorIndex >= 0
|
|
698
|
+
? match.index + match[0].length + nextDecoratorIndex
|
|
699
|
+
: stripped.length;
|
|
700
|
+
const methodSlice = stripped.slice(match.index, sliceEnd);
|
|
701
|
+
const bodyIndex = methodSlice.indexOf("{");
|
|
702
|
+
blocks.push(
|
|
703
|
+
bodyIndex >= 0
|
|
704
|
+
? balancedBlockFrom({
|
|
705
|
+
close: "}",
|
|
706
|
+
open: "{",
|
|
707
|
+
startIndex: match.index + bodyIndex,
|
|
708
|
+
text: stripped,
|
|
709
|
+
})
|
|
710
|
+
: methodSlice,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
for (const match of stripped.matchAll(/\b(?:app|router)\.get\s*\(/g)) {
|
|
714
|
+
blocks.push(
|
|
715
|
+
balancedBlockFrom({
|
|
716
|
+
close: ")",
|
|
717
|
+
open: "(",
|
|
718
|
+
startIndex: match.index,
|
|
719
|
+
text: stripped,
|
|
720
|
+
}),
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
for (const match of stripped.matchAll(
|
|
724
|
+
/\b[A-Za-z_$][\w$]*\.get\s*\(\s*["'`][^"'`]*["'`]\s*,/g,
|
|
725
|
+
)) {
|
|
726
|
+
blocks.push(
|
|
727
|
+
balancedBlockFrom({
|
|
728
|
+
close: ")",
|
|
729
|
+
open: "(",
|
|
730
|
+
startIndex: match.index,
|
|
731
|
+
text: stripped,
|
|
732
|
+
}),
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
return blocks;
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
export const pythonFunctionBlockFrom = ({ startIndex, text }) => {
|
|
739
|
+
const startLineIndex = text.lastIndexOf("\n", startIndex) + 1;
|
|
740
|
+
const lines = text.slice(startLineIndex).split(/\r?\n/);
|
|
741
|
+
const firstLine = lines[0] ?? "";
|
|
742
|
+
const defIndent = firstLine.match(/^\s*/)?.[0].length ?? 0;
|
|
743
|
+
const block = [firstLine];
|
|
744
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
745
|
+
const line = lines[index];
|
|
746
|
+
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
747
|
+
if (line.trim() && indent <= defIndent) break;
|
|
748
|
+
block.push(line);
|
|
749
|
+
}
|
|
750
|
+
return block.join("\n");
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
export const lifecycleHelperNamesInSource = (source) => {
|
|
754
|
+
const stripped = stripSourceNoise(source.text, { stripStrings: true });
|
|
755
|
+
const names = [];
|
|
756
|
+
for (const match of stripped.matchAll(
|
|
757
|
+
/(?:^|\n)\s*(?:async\s+)?def\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*:/g,
|
|
758
|
+
)) {
|
|
759
|
+
const block = pythonFunctionBlockFrom({
|
|
760
|
+
startIndex: match.index,
|
|
761
|
+
text: stripped,
|
|
762
|
+
});
|
|
763
|
+
if (READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(block)) {
|
|
764
|
+
names.push(match[1]);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (const match of stripped.matchAll(
|
|
768
|
+
/(?:^|\n)(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g,
|
|
769
|
+
)) {
|
|
770
|
+
const block = balancedBlockFrom({
|
|
771
|
+
close: "}",
|
|
772
|
+
open: "{",
|
|
773
|
+
startIndex: match.index,
|
|
774
|
+
text: stripped,
|
|
775
|
+
});
|
|
776
|
+
if (READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(block)) {
|
|
777
|
+
names.push(match[1]);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
for (const match of stripped.matchAll(
|
|
781
|
+
/(?:^|\n)\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{/g,
|
|
782
|
+
)) {
|
|
783
|
+
const block = balancedBlockFrom({
|
|
784
|
+
close: "}",
|
|
785
|
+
open: "{",
|
|
786
|
+
startIndex: match.index,
|
|
787
|
+
text: stripped,
|
|
788
|
+
});
|
|
789
|
+
if (READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(block)) {
|
|
790
|
+
names.push(match[1]);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return [...new Set(names)];
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
export const blockCallsLifecycleHelper = (block, helperNames) =>
|
|
797
|
+
helperNames.some((name) =>
|
|
798
|
+
new RegExp(`\\b${escapeRegex(name)}\\s*\\(`).test(block),
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
export const backendImportCandidates = ({ importerPath, specifier }) => {
|
|
802
|
+
let base = null;
|
|
803
|
+
if (specifier.startsWith(".")) {
|
|
804
|
+
base = join(dirname(importerPath), specifier).replaceAll("\\", "/");
|
|
805
|
+
} else if (/^[A-Za-z_$][\w$.]*$/.test(specifier)) {
|
|
806
|
+
base = join(
|
|
807
|
+
dirname(importerPath),
|
|
808
|
+
specifier.replaceAll(".", "/"),
|
|
809
|
+
).replaceAll("\\", "/");
|
|
810
|
+
}
|
|
811
|
+
if (!base) return [];
|
|
812
|
+
return [
|
|
813
|
+
base,
|
|
814
|
+
`${base}.py`,
|
|
815
|
+
`${base}.ts`,
|
|
816
|
+
`${base}.tsx`,
|
|
817
|
+
`${base}.js`,
|
|
818
|
+
`${base}.jsx`,
|
|
819
|
+
join(base, "__init__.py").replaceAll("\\", "/"),
|
|
820
|
+
join(base, "index.ts").replaceAll("\\", "/"),
|
|
821
|
+
join(base, "index.tsx").replaceAll("\\", "/"),
|
|
822
|
+
join(base, "index.js").replaceAll("\\", "/"),
|
|
823
|
+
join(base, "index.jsx").replaceAll("\\", "/"),
|
|
824
|
+
];
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
export const backendImportedLifecycleAliasNames = ({ backendSources, source }) => {
|
|
828
|
+
const sourceByPath = new Map(
|
|
829
|
+
backendSources.map((entry) => [entry.file_path, entry]),
|
|
830
|
+
);
|
|
831
|
+
const aliases = new Set();
|
|
832
|
+
const text = stripSourceNoise(source.text);
|
|
833
|
+
|
|
834
|
+
for (const match of text.matchAll(
|
|
835
|
+
/\bimport\s+([^;]+?)\s+from\s+["']([^"']+)["']/g,
|
|
836
|
+
)) {
|
|
837
|
+
const importedSource = backendImportCandidates({
|
|
838
|
+
importerPath: source.file_path,
|
|
839
|
+
specifier: match[2],
|
|
840
|
+
})
|
|
841
|
+
.map((candidate) => sourceByPath.get(candidate))
|
|
842
|
+
.find(Boolean);
|
|
843
|
+
if (!importedSource) continue;
|
|
844
|
+
const exportedLifecycleNames = new Set(
|
|
845
|
+
lifecycleHelperNamesInSource(importedSource),
|
|
846
|
+
);
|
|
847
|
+
const importedSourceHasLifecycle = READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(
|
|
848
|
+
stripSourceNoise(importedSource.text, { stripStrings: true }),
|
|
849
|
+
);
|
|
850
|
+
const namedImport = /\{([\s\S]*)\}/.exec(match[1].trim());
|
|
851
|
+
if (!namedImport) continue;
|
|
852
|
+
for (const rawPart of namedImport[1].split(",")) {
|
|
853
|
+
const part = rawPart.trim();
|
|
854
|
+
if (!part) continue;
|
|
855
|
+
const [exported, local = exported] = part
|
|
856
|
+
.split(/\s+as\s+/i)
|
|
857
|
+
.map((entry) => entry.trim());
|
|
858
|
+
if (exportedLifecycleNames.has(exported) || importedSourceHasLifecycle) {
|
|
859
|
+
aliases.add(local);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
for (const match of text.matchAll(
|
|
865
|
+
/\bfrom\s+([A-Za-z_$][\w$.]*|\.[A-Za-z_$][\w$.]*)\s+import\s+([^\n]+)/g,
|
|
866
|
+
)) {
|
|
867
|
+
const importedSource = backendImportCandidates({
|
|
868
|
+
importerPath: source.file_path,
|
|
869
|
+
specifier: match[1],
|
|
870
|
+
})
|
|
871
|
+
.map((candidate) => sourceByPath.get(candidate))
|
|
872
|
+
.find(Boolean);
|
|
873
|
+
if (!importedSource) continue;
|
|
874
|
+
const exportedLifecycleNames = new Set(
|
|
875
|
+
lifecycleHelperNamesInSource(importedSource),
|
|
876
|
+
);
|
|
877
|
+
const importedSourceHasLifecycle = READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(
|
|
878
|
+
stripSourceNoise(importedSource.text, { stripStrings: true }),
|
|
879
|
+
);
|
|
880
|
+
for (const rawPart of match[2].split(",")) {
|
|
881
|
+
const part = rawPart.trim();
|
|
882
|
+
if (!part || part === "*") continue;
|
|
883
|
+
const [exported, local = exported] = part
|
|
884
|
+
.split(/\s+as\s+/i)
|
|
885
|
+
.map((entry) => entry.trim());
|
|
886
|
+
if (exportedLifecycleNames.has(exported) || importedSourceHasLifecycle) {
|
|
887
|
+
aliases.add(local);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return [...aliases];
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
export const importedLocalNamesInSource = (source) => {
|
|
896
|
+
const text = stripSourceNoise(source.text);
|
|
897
|
+
const names = [];
|
|
898
|
+
for (const match of text.matchAll(
|
|
899
|
+
/\bimport\s+([^;]+?)\s+from\s+["'][^"']+["']/g,
|
|
900
|
+
)) {
|
|
901
|
+
const namedImport = /\{([\s\S]*)\}/.exec(match[1].trim());
|
|
902
|
+
if (!namedImport) continue;
|
|
903
|
+
for (const rawPart of namedImport[1].split(",")) {
|
|
904
|
+
const part = rawPart.trim();
|
|
905
|
+
if (!part) continue;
|
|
906
|
+
const [, local = part] = part
|
|
907
|
+
.split(/\s+as\s+/i)
|
|
908
|
+
.map((entry) => entry.trim());
|
|
909
|
+
names.push(local);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (const match of text.matchAll(
|
|
913
|
+
/\bfrom\s+[A-Za-z_$.\w$]+\s+import\s+([^\n]+)/g,
|
|
914
|
+
)) {
|
|
915
|
+
for (const rawPart of match[1].split(",")) {
|
|
916
|
+
const part = rawPart.trim();
|
|
917
|
+
if (!part || part === "*") continue;
|
|
918
|
+
const [, local = part] = part
|
|
919
|
+
.split(/\s+as\s+/i)
|
|
920
|
+
.map((entry) => entry.trim());
|
|
921
|
+
names.push(local);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return names.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
export const findBackendLifecycleReadEndpointFiles = (backendSources) => {
|
|
928
|
+
const globalLifecycleHelperNames = new Set(
|
|
929
|
+
backendSources.flatMap((source) => lifecycleHelperNamesInSource(source)),
|
|
930
|
+
);
|
|
931
|
+
return backendSources
|
|
932
|
+
.filter((source) => {
|
|
933
|
+
const helperNames = [
|
|
934
|
+
...lifecycleHelperNamesInSource(source),
|
|
935
|
+
...backendImportedLifecycleAliasNames({ backendSources, source }),
|
|
936
|
+
...importedLocalNamesInSource(source).filter((name) =>
|
|
937
|
+
globalLifecycleHelperNames.has(name),
|
|
938
|
+
),
|
|
939
|
+
];
|
|
940
|
+
return [
|
|
941
|
+
...pythonGetEndpointBlocks(source.text),
|
|
942
|
+
...typescriptGetEndpointBlocks(source.text),
|
|
943
|
+
].some(
|
|
944
|
+
(block) =>
|
|
945
|
+
READ_SIDE_EFFECT_LIFECYCLE_PATTERN.test(block) ||
|
|
946
|
+
blockCallsLifecycleHelper(block, helperNames),
|
|
947
|
+
);
|
|
948
|
+
})
|
|
949
|
+
.map((source) => source.file_path);
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
export const lifecycleConceptPattern = (concepts) =>
|
|
953
|
+
new RegExp(
|
|
954
|
+
concepts
|
|
955
|
+
.flatMap((concept) => [
|
|
956
|
+
concept,
|
|
957
|
+
`${concept}[_-]?id`,
|
|
958
|
+
`${concept}Id`,
|
|
959
|
+
`${concept}ID`,
|
|
960
|
+
])
|
|
961
|
+
.map((value) => `\\b${value}\\b`)
|
|
962
|
+
.join("|"),
|
|
963
|
+
"i",
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
export const identityBoundaryPattern = lifecycleConceptPattern([
|
|
967
|
+
"login",
|
|
968
|
+
"signin",
|
|
969
|
+
"sign_in",
|
|
970
|
+
"auth",
|
|
971
|
+
"token",
|
|
972
|
+
"session",
|
|
973
|
+
"user",
|
|
974
|
+
]);
|
|
975
|
+
export const frontendIdentityBoundaryPattern = lifecycleConceptPattern([
|
|
976
|
+
"login",
|
|
977
|
+
"signin",
|
|
978
|
+
"sign_in",
|
|
979
|
+
"auth",
|
|
980
|
+
"session",
|
|
981
|
+
"user",
|
|
982
|
+
]);
|
|
983
|
+
export const logoutBoundaryPattern = /\b(logout|signout|sign_out)\b/i;
|
|
984
|
+
// Source app vocabulary that may represent the company boundary Cluebase maps to
|
|
985
|
+
// organization. These are not Cluebase account/workspace concepts.
|
|
986
|
+
export const companyBoundaryPattern = lifecycleConceptPattern([
|
|
987
|
+
"account",
|
|
988
|
+
"workspace",
|
|
989
|
+
"company",
|
|
990
|
+
"organization",
|
|
991
|
+
"org",
|
|
992
|
+
"tenant",
|
|
993
|
+
]);
|
|
994
|
+
|