@groma/scanner-react 0.1.4 → 0.2.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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // plugins/scanners/react/src/index.ts
3
- import ts5 from "./typescript.cjs";
3
+ import ts7 from "./typescript.cjs";
4
4
 
5
5
  // plugins/scanners/typescript-outline.ts
6
6
  import { readFile } from "fs/promises";
@@ -71,7 +71,7 @@ function typeMember(scope, member) {
71
71
  return name === undefined ? [] : [symbolAt(scope, name.text, name, memberVisibility(scope, member))];
72
72
  }
73
73
  function typeMembers(k, statement) {
74
- if (is(statement, k.ClassDeclaration) || is(statement, k.InterfaceDeclaration))
74
+ if (is(statement, k.ClassDeclaration) || is(statement, k.ClassExpression) || is(statement, k.InterfaceDeclaration))
75
75
  return statement.members;
76
76
  return statement.kind === k.EnumDeclaration ? [] : undefined;
77
77
  }
@@ -100,9 +100,12 @@ function declarationsIn(scope, statements) {
100
100
  return [...functionDeclarations(scope, statement), ...typeDeclaration(scope, statement)];
101
101
  });
102
102
  }
103
+ function listedExportNames(ts, source) {
104
+ return new Set(source.statements.flatMap((statement) => listedExports(ts.SyntaxKind, statement)));
105
+ }
103
106
  function outlineSource(ts, source, context) {
104
107
  const k = ts.SyntaxKind;
105
- const exported = new Set([...source.statements.flatMap((statement) => listedExports(k, statement)), ...context.exported ?? []]);
108
+ const exported = new Set([...listedExportNames(ts, source), ...context.exported ?? []]);
106
109
  const scope = { k, source, symbols: context.symbols, exported, topLevelPrivate: context.topLevelPrivate ?? false };
107
110
  return declarationsIn(scope, source.statements);
108
111
  }
@@ -121,311 +124,17 @@ async function readTypeScriptOutline(ts, repositoryRoot, references) {
121
124
  return files;
122
125
  }
123
126
 
124
- // plugins/scanners/typescript-project.ts
125
- import path3 from "path";
126
-
127
- // plugins/scanners/projects.ts
128
- import { execFile } from "child_process";
129
- import { existsSync } from "fs";
130
- import { readFile as readFile2 } from "fs/promises";
131
- import path2 from "path";
132
- import { promisify } from "util";
133
- var execute = promisify(execFile);
134
- var excluded = new Set([
135
- ".git",
136
- "node_modules",
137
- "vendor",
138
- "target",
139
- "dist",
140
- "build",
141
- "bin",
142
- "obj",
143
- ".gradle",
144
- ".angular",
145
- "coverage",
146
- "generated",
147
- "groma",
148
- ".groma"
149
- ]);
150
- async function repositoryFiles(root, matches) {
151
- const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
152
- return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path2.join(root, file))))].sort();
153
- }
154
- function projectFiles(root, matches) {
155
- return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
156
- }
157
- function hasDependency(manifest, dependency) {
158
- return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some((section) => typeof manifest[section]?.[dependency] === "string");
159
- }
160
- async function frameworkProjects(root, dependency, extensions) {
161
- const files = await projectFiles(root, () => true);
162
- const manifests = files.filter((file) => path2.posix.basename(file) === "package.json");
163
- const directories = new Set(manifests.map((file) => path2.posix.dirname(file)));
164
- const sources = new Set;
165
- for (const file of files.filter((file) => extensions.some((extension) => file.endsWith(extension)) && !file.endsWith(".d.ts"))) {
166
- let directory = path2.posix.dirname(file);
167
- while (directory !== "." && !directories.has(directory))
168
- directory = path2.posix.dirname(directory);
169
- sources.add(directory);
170
- }
171
- const projects = [];
172
- for (const file of manifests) {
173
- const directory = path2.posix.dirname(file);
174
- if (!sources.has(directory) || !files.includes(path2.posix.join(directory, "tsconfig.json")))
175
- continue;
176
- const manifest = JSON.parse(await readFile2(path2.join(root, file), "utf8"));
177
- if (hasDependency(manifest, dependency))
178
- projects.push(path2.dirname(path2.join(root, file)));
179
- }
180
- return projects;
127
+ // plugins/scanners/typescript/src/files.ts
128
+ function typeScriptSources(files) {
129
+ return files.filter((file) => /\.tsx?$/.test(file) && !file.endsWith(".d.ts"));
181
130
  }
182
131
 
183
- // plugins/scanners/typescript-project.ts
184
- async function frameworkSourceFiles(options) {
185
- const { root, dependency, projects, sources, also } = options;
186
- const directories = (await frameworkProjects(root, dependency, projects)).map((directory) => path3.relative(root, directory).split(path3.sep).join("/") || ".");
187
- if (directories.length === 0)
188
- return [];
189
- const listed = [];
190
- for (const file of await projectFiles(root, (candidate) => !candidate.endsWith(".d.ts"))) {
191
- const project = directories.find((directory) => directory === "." || file.startsWith(`${directory}/`));
192
- if (project === undefined)
193
- continue;
194
- const local = project === "." ? file : file.slice(project.length + 1);
195
- if (sources.some((extension) => file.endsWith(extension)) || (also?.(local) ?? false))
196
- listed.push(file);
197
- }
198
- return listed.sort();
199
- }
132
+ // plugins/scanners/react/src/scan.ts
133
+ import path8 from "path";
200
134
 
201
- // plugins/scanners/react/src/routes.ts
202
- import { existsSync as existsSync2 } from "fs";
135
+ // plugins/scanners/entry-points/javascript.ts
136
+ import { readFile as readFile3 } from "fs/promises";
203
137
  import path4 from "path";
204
- import ts2 from "./typescript.cjs";
205
-
206
- // plugins/scanners/http-url.ts
207
- var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
208
- var absolute = /^([A-Za-z][A-Za-z\d+.-]*:)?\/\//;
209
- var HOLE = "\x00";
210
- var computedPart = { kind: "hole", configured: false };
211
- var configuredPart = { kind: "hole", configured: true };
212
- var methodToken = /^[A-Z][A-Z-]*$/;
213
- function methodText(text) {
214
- const method = text?.toUpperCase();
215
- return method !== undefined && methodToken.test(method) ? method : undefined;
216
- }
217
- function requestSegment(part) {
218
- if (part === HOLE)
219
- return { kind: "dynamic" };
220
- if (part.includes(HOLE) || !pathText.test(part))
221
- return { kind: "unknown" };
222
- return { kind: "literal", value: part };
223
- }
224
- function segmentsOf(parts) {
225
- const url = parts.map((part) => part.kind === "text" ? part.text : HOLE).join("");
226
- const end = Math.min(...["?", "#"].map((mark) => url.includes(mark) ? url.indexOf(mark) : url.length));
227
- return url.slice(0, end).split("/").filter((part) => part !== "").map(requestSegment);
228
- }
229
- function afterAuthority(text) {
230
- const slash = text.indexOf("/", text.indexOf("//") + 2);
231
- return slash < 0 ? "" : text.slice(slash);
232
- }
233
- function joinedText(input) {
234
- const parts = [];
235
- for (const part of input) {
236
- const last = parts.at(-1);
237
- if (part.kind === "text" && last?.kind === "text")
238
- parts[parts.length - 1] = { kind: "text", text: last.text + part.text };
239
- else if (part.kind === "hole" || part.text !== "")
240
- parts.push(part);
241
- }
242
- return parts;
243
- }
244
- function requestUrl(input) {
245
- const parts = joinedText(input);
246
- const [first, ...rest] = parts;
247
- if (first === undefined)
248
- return { path: [{ kind: "unknown" }] };
249
- if (first.kind === "hole") {
250
- const path = segmentsOf(rest);
251
- const next = rest[0];
252
- const separated = next === undefined || next.kind === "text" && next.text.startsWith("/");
253
- return first.configured && separated ? { configured: true, path } : { path: [{ kind: "unknown" }, ...path] };
254
- }
255
- if (absolute.test(first.text)) {
256
- return { path: [{ kind: "unknown" }, ...segmentsOf([{ kind: "text", text: afterAuthority(first.text) }, ...rest])] };
257
- }
258
- if (!first.text.startsWith("/"))
259
- return { path: [{ kind: "unknown" }, ...segmentsOf(parts)] };
260
- return { path: segmentsOf(parts) };
261
- }
262
- function joinBase(base, url) {
263
- const stated = joinedText(base);
264
- const path = joinedText(url);
265
- const [first] = path;
266
- if (stated.length === 0)
267
- return path;
268
- if (first === undefined)
269
- return stated;
270
- if (first.kind === "hole")
271
- return [computedPart, ...path.slice(1)];
272
- return absolute.test(first.text) ? path : [...stated, { kind: "text", text: "/" }, ...path];
273
- }
274
- function withBase(base, url) {
275
- const stated = joinedText(base);
276
- const path = joinedText(url);
277
- const [only] = stated;
278
- const [first, ...rest] = path;
279
- if (stated.length !== 1 || only?.kind !== "text" || first?.kind !== "text" || absolute.test(first.text)) {
280
- return joinBase(stated, path);
281
- }
282
- const prefix = only.text.replace(/\/$/, "");
283
- if (first.text.startsWith(prefix)) {
284
- const next = first.text[prefix.length];
285
- if (next === undefined)
286
- return rest.length === 0 ? path : [computedPart, ...rest];
287
- return next === "/" || next === "?" ? path : joinBase(stated, path);
288
- }
289
- return prefix.startsWith(first.text) && rest.length > 0 ? [computedPart, ...rest] : joinBase(stated, path);
290
- }
291
-
292
- // plugins/scanners/react/src/functions.ts
293
- import ts from "./typescript.cjs";
294
- function executable(node) {
295
- return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) && !!node.body;
296
- }
297
-
298
- // plugins/scanners/react/src/routes.ts
299
- var METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
300
- var APP_ROUTE = /^(?:(.+)\/)?route\.tsx?$/;
301
- var PAGES_API = /^(api\/.+)\.tsx?$/;
302
- var OPTIONAL_CATCH_ALL = /^\[\[\.\.\.(.+)\]\]$/;
303
- var CATCH_ALL = /^\[\.\.\.(.+)\]$/;
304
- var PARAMETER = /^\[(.+)\]$/;
305
- function nextRouters(projectRoot) {
306
- const active = (name) => existsSync2(path4.join(projectRoot, name)) ? name : `src/${name}`;
307
- return { app: active("app"), pages: active("pages") };
308
- }
309
- function routeLocation(file, routers) {
310
- const within = (directory) => file.startsWith(`${directory}/`) ? file.slice(directory.length + 1) : undefined;
311
- const app = APP_ROUTE.exec(within(routers.app) ?? "");
312
- if (app)
313
- return { router: "app", ...app[1] === undefined ? {} : { directory: app[1] } };
314
- const pages = PAGES_API.exec(within(routers.pages) ?? "");
315
- return pages ? { router: "pages", directory: pages[1] } : undefined;
316
- }
317
- function routeCandidate(file) {
318
- return ["", "src/"].some((prefix) => routeLocation(file, { app: `${prefix}app`, pages: `${prefix}pages` }) !== undefined);
319
- }
320
- var GROUP = /^\((?!\.+\))[^)]+\)$/;
321
- function routeSegment(part, last) {
322
- const optional = OPTIONAL_CATCH_ALL.exec(part);
323
- if (optional)
324
- return last && pathText.test(optional[1]) ? { kind: "catch-all", name: optional[1], optional: true } : undefined;
325
- const rest = CATCH_ALL.exec(part);
326
- if (rest)
327
- return last && pathText.test(rest[1]) ? { kind: "catch-all", name: rest[1] } : undefined;
328
- const parameter = PARAMETER.exec(part);
329
- if (parameter)
330
- return pathText.test(parameter[1]) ? { kind: "parameter", name: parameter[1] } : undefined;
331
- if (part.startsWith("(") || part.startsWith("@") || part.includes("[") || part.includes("]"))
332
- return;
333
- return pathText.test(part) ? { kind: "literal", value: part } : undefined;
334
- }
335
- function routeParts(directory, stripIndex) {
336
- const parts = (directory ?? "").split("/").filter((part) => part !== "" && !GROUP.test(part));
337
- return stripIndex && parts.at(-1) === "index" ? parts.slice(0, -1) : parts;
338
- }
339
- function routePath(directory, stripIndex) {
340
- const parts = routeParts(directory, stripIndex);
341
- const segments = [];
342
- for (const [index, part] of parts.entries()) {
343
- const segment = routeSegment(part, index === parts.length - 1);
344
- if (segment === undefined)
345
- return;
346
- segments.push(segment);
347
- }
348
- return segments;
349
- }
350
- function functionValue(node) {
351
- return node !== undefined && executable(node) ? node : undefined;
352
- }
353
- function hasModifier2(statement, kind) {
354
- return ts2.canHaveModifiers(statement) && (ts2.getModifiers(statement) ?? []).some((modifier) => modifier.kind === kind);
355
- }
356
- function declaredHandler(statement) {
357
- if (!ts2.isFunctionDeclaration(statement) || statement.name === undefined)
358
- return;
359
- const handler = functionValue(statement);
360
- return handler !== undefined && METHODS.has(statement.name.text) ? [statement.name.text, handler] : undefined;
361
- }
362
- function boundHandlers(statement) {
363
- if (!ts2.isVariableStatement(statement))
364
- return [];
365
- return statement.declarationList.declarations.flatMap((declaration) => {
366
- const handler = functionValue(declaration.initializer);
367
- if (handler === undefined || !ts2.isIdentifier(declaration.name) || !METHODS.has(declaration.name.text))
368
- return [];
369
- return [[declaration.name.text, handler]];
370
- });
371
- }
372
- function appHandlers(source) {
373
- const exported = source.statements.filter((statement) => hasModifier2(statement, ts2.SyntaxKind.ExportKeyword));
374
- return new Map(exported.flatMap((statement) => {
375
- const declared = declaredHandler(statement);
376
- return declared === undefined ? boundHandlers(statement) : [declared];
377
- }));
378
- }
379
- function localFunction(source, name) {
380
- for (const statement of source.statements) {
381
- if (ts2.isFunctionDeclaration(statement) && statement.name?.text === name)
382
- return functionValue(statement);
383
- if (!ts2.isVariableStatement(statement))
384
- continue;
385
- for (const declaration of statement.declarationList.declarations) {
386
- if (ts2.isIdentifier(declaration.name) && declaration.name.text === name)
387
- return functionValue(declaration.initializer);
388
- }
389
- }
390
- return;
391
- }
392
- function pagesHandler(source) {
393
- for (const statement of source.statements) {
394
- if (ts2.isFunctionDeclaration(statement) && hasModifier2(statement, ts2.SyntaxKind.DefaultKeyword))
395
- return functionValue(statement);
396
- if (!ts2.isExportAssignment(statement) || statement.isExportEquals === true)
397
- continue;
398
- const value = functionValue(statement.expression);
399
- if (value !== undefined)
400
- return value;
401
- if (ts2.isIdentifier(statement.expression))
402
- return localFunction(source, statement.expression.text);
403
- }
404
- return;
405
- }
406
- function nextRouteEndpoints(sources, projectRoot, routers, operationId) {
407
- const endpoints = [];
408
- for (const source of sources) {
409
- const location = routeLocation(path4.relative(projectRoot, source.fileName).split(path4.sep).join("/"), routers);
410
- const segments = location === undefined ? undefined : routePath(location.directory, location.router === "pages");
411
- if (location === undefined || segments === undefined)
412
- continue;
413
- if (location.router === "app") {
414
- for (const [method, handler] of appHandlers(source)) {
415
- endpoints.push({ operation: operationId(handler), method, path: segments });
416
- }
417
- continue;
418
- }
419
- const handler = pagesHandler(source);
420
- if (handler !== undefined)
421
- endpoints.push({ operation: operationId(handler), method: "*", path: segments });
422
- }
423
- return endpoints;
424
- }
425
-
426
- // plugins/scanners/react/src/scan.ts
427
- import { existsSync as existsSync3, readFileSync } from "fs";
428
- import path5 from "path";
429
138
 
430
139
  // packages/scanner/src/values.ts
431
140
  function object(value, label) {
@@ -446,18 +155,18 @@ function array(value, label) {
446
155
  }
447
156
 
448
157
  // packages/scanner/src/http.ts
449
- var pathText2 = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
450
- var methodToken2 = /^[A-Z][A-Z-]*$/;
158
+ var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
159
+ var methodToken = /^[A-Z][A-Z-]*$/;
451
160
  function fail(message) {
452
161
  throw new Error(message);
453
162
  }
454
163
  function text(value, label) {
455
- if (typeof value !== "string" || !pathText2.test(value))
164
+ if (typeof value !== "string" || !pathText.test(value))
456
165
  fail(`${label} must be nonempty URL path characters`);
457
166
  return value;
458
167
  }
459
168
  function method(value, label, anyMethod) {
460
- if (typeof value !== "string" || !(methodToken2.test(value) || anyMethod && value === "*")) {
169
+ if (typeof value !== "string" || !(methodToken.test(value) || anyMethod && value === "*")) {
461
170
  fail(`${label} must be an uppercase HTTP method${anyMethod ? " or *" : ""}`);
462
171
  }
463
172
  return value;
@@ -486,7 +195,7 @@ function endpointSegment(value, last) {
486
195
  ...segment.constrained ? { constrained: true } : {}
487
196
  };
488
197
  }
489
- function requestSegment2(value) {
198
+ function requestSegment(value) {
490
199
  const segment = object(value, "HTTP request segment");
491
200
  if (segment.kind === "literal")
492
201
  return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
@@ -523,7 +232,7 @@ function request(value, operations) {
523
232
  operation: operation(fact.operation, operations),
524
233
  ...fact.method === undefined ? {} : { method: method(fact.method, "HTTP request method", false) },
525
234
  ...fact.configured ? { configured: true } : {},
526
- path: array(fact.path, "HTTP request path").map(requestSegment2)
235
+ path: array(fact.path, "HTTP request path").map(requestSegment)
527
236
  };
528
237
  }
529
238
  function httpEvidence(endpoints, requests, operations) {
@@ -581,6 +290,7 @@ function createScanObservation(input) {
581
290
  roots,
582
291
  files,
583
292
  ...sourceUnits(input.sourceUnits, filePaths),
293
+ ...entryPoints(input.entryPoints, filePaths),
584
294
  ...operationEvidence(input, filePaths),
585
295
  ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
586
296
  diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
@@ -591,6 +301,29 @@ function createScanObservation(input) {
591
301
  })
592
302
  };
593
303
  }
304
+ function entryPoints(input, paths) {
305
+ if (input === undefined)
306
+ return {};
307
+ const entries = array(input, "entryPoints").map((value) => {
308
+ const entry = object(value, "entry point");
309
+ const files = [...new Set(array(entry.files, "entry point.files").map((file) => string(file, "entry point.file")))].sort();
310
+ for (const file of files) {
311
+ if (!paths.has(file))
312
+ throw new Error(`entry point references unknown file: ${file}`);
313
+ }
314
+ const file = string(entry.file, "entry point.file");
315
+ if (!files.includes(file))
316
+ throw new Error(`entry point must include its own source file: ${file}`);
317
+ return {
318
+ file,
319
+ declaration: string(entry.declaration, "entry point.declaration"),
320
+ name: string(entry.name, "entry point.name"),
321
+ files
322
+ };
323
+ });
324
+ const key = (entry) => compare(entry.file, entry.declaration, entry.name, ...entry.files);
325
+ return { entryPoints: [...new Map(entries.map((entry) => [key(entry), entry])).values()].sort((a, b) => key(a).localeCompare(key(b))) };
326
+ }
594
327
  function sourceUnits(input, paths) {
595
328
  if (input === undefined)
596
329
  return {};
@@ -708,52 +441,217 @@ function sourcePosition(position) {
708
441
  return { position: Number(position) };
709
442
  }
710
443
 
711
- // plugins/scanners/react/src/scan.ts
712
- import ts4 from "./typescript.cjs";
444
+ // plugins/scanners/workspace-packages.ts
445
+ import path3 from "path";
713
446
 
714
- // plugins/scanners/observations.ts
715
- function combineObservations(parts) {
716
- if (!parts.length)
447
+ // plugins/scanners/typescript-project.ts
448
+ import { readFile as readFile2 } from "fs/promises";
449
+ import path2 from "path";
450
+ async function packageManifest(root, file) {
451
+ try {
452
+ return JSON.parse(await readFile2(path2.join(root, file), "utf8"));
453
+ } catch {
717
454
  return;
718
- const roots = [];
719
- const files = new Map;
720
- const operations = [];
721
- const invocations = [];
722
- const diagnostics = [];
723
- const sourceUnits = [];
724
- const httpEndpoints = [];
725
- const httpRequests = [];
726
- for (const { key, observation } of parts) {
727
- const id = (value) => JSON.stringify([key, value]);
728
- roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
729
- for (const source of observation.files) {
730
- const combined = files.get(source.file) ?? { file: source.file, roots: [], symbols: [] };
731
- combined.roots.push(...source.roots.map(id));
732
- combined.symbols.push(...source.symbols.map((symbol) => ({ ...symbol, id: id(symbol.id) })));
733
- files.set(source.file, combined);
734
- }
735
- operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
736
- invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
737
- httpEndpoints.push(...(observation.httpEndpoints ?? []).map((endpoint) => ({ ...endpoint, operation: id(endpoint.operation) })));
738
- httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
739
- diagnostics.push(...observation.diagnostics);
740
- sourceUnits.push(...observation.sourceUnits ?? []);
741
455
  }
742
- return createScanObservation({
743
- scanner: parts[0].observation.scanner,
744
- roots,
745
- files: [...files.values()],
746
- operations,
747
- invocations,
748
- diagnostics,
749
- ...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
750
- ...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
751
- ...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
456
+ }
457
+ function hasDependency(manifest, dependency) {
458
+ return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some((section) => typeof manifest[section]?.[dependency] === "string");
459
+ }
460
+ function hasProjectConfig(directory, configs, options) {
461
+ if (options.nestedConfig && [...configs].some((config) => directory === "." || config.startsWith(`${directory}/`)))
462
+ return true;
463
+ for (let current = directory;; current = path2.posix.dirname(current)) {
464
+ if (configs.has(current))
465
+ return true;
466
+ if (!options.inheritConfig || current === ".")
467
+ return false;
468
+ }
469
+ }
470
+ async function frameworkProjects(root, files, dependency, extensions, options = {}) {
471
+ const manifests = files.filter((file) => path2.posix.basename(file) === "package.json");
472
+ const configs = new Set(files.filter((file) => path2.posix.basename(file) === "tsconfig.json").map((file) => path2.posix.dirname(file)));
473
+ const candidates = new Set;
474
+ for (const file of manifests) {
475
+ const directory = path2.posix.dirname(file);
476
+ if (!hasProjectConfig(directory, configs, options))
477
+ continue;
478
+ const manifest = await packageManifest(root, file);
479
+ if (manifest && hasDependency(manifest, dependency))
480
+ candidates.add(directory);
481
+ }
482
+ const sources = new Set;
483
+ for (const file of files.filter((file) => extensions.some((extension) => file.endsWith(extension)) && !file.endsWith(".d.ts"))) {
484
+ let directory = path2.posix.dirname(file);
485
+ while (directory !== "." && !candidates.has(directory))
486
+ directory = path2.posix.dirname(directory);
487
+ if (candidates.has(directory))
488
+ sources.add(directory);
489
+ }
490
+ return [...candidates].filter((directory) => sources.has(directory)).map((directory) => path2.resolve(root, directory));
491
+ }
492
+
493
+ // plugins/scanners/workspace-packages.ts
494
+ function inRootDir(file, outputs) {
495
+ const output = outputs.find(({ outDir }) => file.startsWith(`${outDir}/`));
496
+ return output === undefined ? undefined : path3.posix.join(output.rootDir, file.slice(output.outDir.length + 1));
497
+ }
498
+ function sourceOf(file, outputs, files) {
499
+ const stem = inRootDir(file, outputs)?.replace(/\.(?:d\.[cm]?ts|[cm]?jsx?)$/, "");
500
+ return stem === undefined ? undefined : [".ts", ".tsx"].map((extension) => `${stem}${extension}`).find((candidate) => files.has(candidate));
501
+ }
502
+ var conditions = new Set(["types", "import", "default", "require"]);
503
+
504
+ // plugins/scanners/entry-points/javascript.ts
505
+ function packageFor(file, packages) {
506
+ let directory = path4.posix.dirname(file);
507
+ while (directory !== "." && !packages.has(directory))
508
+ directory = path4.posix.dirname(directory);
509
+ return directory;
510
+ }
511
+ function declaredBins(declaration, manifest) {
512
+ const bins = typeof manifest.bin === "string" ? { [String(manifest.name ?? "main")]: manifest.bin } : manifest.bin;
513
+ if (!bins || typeof bins !== "object")
514
+ return [];
515
+ return Object.entries(bins).flatMap(([name, target]) => typeof target === "string" ? [{ declaration, name, file: path4.posix.join(path4.posix.dirname(declaration), target) }] : []);
516
+ }
517
+ var RUNTIMES = new Set(["node", "bun", "tsx", "ts-node"]);
518
+ var VALUE_OPTIONS = new Set(["-r", "--require", "--import", "--tsconfig"]);
519
+ var SCRIPT = /^[\w./-]+\.[cm]?[jt]sx?$/;
520
+ function runtimeAt(words) {
521
+ let index = words[0] === "cross-env" ? 1 : 0;
522
+ while (/^[A-Za-z_]\w*=/.test(words[index] ?? ""))
523
+ index += 1;
524
+ return index;
525
+ }
526
+ function commandScript(command) {
527
+ const words = command.trim().split(/\s+/);
528
+ let index = runtimeAt(words);
529
+ if (!RUNTIMES.has(words[index] ?? ""))
530
+ return;
531
+ let subcommand = true;
532
+ for (index += 1;index < words.length; index += 1) {
533
+ const word = words[index];
534
+ if (VALUE_OPTIONS.has(word))
535
+ index += 1;
536
+ else if (subcommand && (word === "run" || word === "watch"))
537
+ subcommand = false;
538
+ else if (!word.startsWith("-"))
539
+ return SCRIPT.test(word) ? word : undefined;
540
+ }
541
+ return;
542
+ }
543
+ function declaredScripts(declaration, scripts) {
544
+ return Object.entries(scripts ?? {}).flatMap(([name, script]) => {
545
+ if (typeof script !== "string")
546
+ return [];
547
+ const commands = /["'`]/.test(script) ? [script] : script.split(/\s+&&\s+/);
548
+ return commands.flatMap((command) => {
549
+ const file = commandScript(command);
550
+ return file === undefined ? [] : [{ declaration, name, file: path4.posix.join(path4.posix.dirname(declaration), file) }];
551
+ });
552
+ });
553
+ }
554
+ var isString = (value) => typeof value === "string";
555
+ function buildEntry(declaration, workspace, name, project) {
556
+ const build = (project.architect ?? project.targets)?.build;
557
+ const main = build?.options?.browser ?? build?.options?.main;
558
+ if (!isString(main))
559
+ return [];
560
+ const replacements = Object.values(build?.configurations ?? {}).flatMap((configuration) => Array.isArray(configuration.fileReplacements) ? configuration.fileReplacements.map((replacement) => replacement?.with) : []);
561
+ const at = (file) => path4.posix.join(workspace, file);
562
+ return [{ declaration, name, file: at(main), compiled: [build?.options?.polyfills, ...replacements].flat().filter(isString).map(at) }];
563
+ }
564
+ function angularEntries(declaration, source) {
565
+ const projects = JSON.parse(source).projects ?? {};
566
+ return Object.entries(projects).flatMap(([name, project]) => buildEntry(declaration, path4.posix.dirname(declaration), name, project));
567
+ }
568
+ function nxEntries(declaration, source, workspaces) {
569
+ const workspace = packageFor(declaration, workspaces);
570
+ if (!workspaces.has(workspace))
571
+ return [];
572
+ const project = JSON.parse(source);
573
+ return buildEntry(declaration, workspace, project.name ?? path4.posix.basename(path4.posix.dirname(declaration)), project);
574
+ }
575
+ function browserEntries(declaration, source, packages) {
576
+ const scripts = [...source.matchAll(/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi)];
577
+ const folder = path4.posix.dirname(declaration);
578
+ const page = path4.posix.basename(declaration, ".html");
579
+ const name = page === "index" && folder !== "." ? path4.posix.basename(folder) : page;
580
+ return scripts.flatMap((match) => {
581
+ const src = match[1];
582
+ if (/^(?:[a-z]+:|\/\/)/i.test(src))
583
+ return [];
584
+ const base = src.startsWith("/") ? packageFor(declaration, packages) : folder;
585
+ return [{ file: path4.posix.join(base, src.replace(/^\//, "")), declaration, name }];
586
+ });
587
+ }
588
+ function reachedFiles(entry, compiled, imports, packages, units) {
589
+ const owner = packageFor(entry, packages), found = new Set;
590
+ const pending = [entry, ...compiled];
591
+ while (pending.length) {
592
+ const file = pending.pop();
593
+ if (found.has(file) || packageFor(file, packages) !== owner)
594
+ continue;
595
+ found.add(file);
596
+ pending.push(...imports.get(file) ?? []);
597
+ }
598
+ for (const unit of units)
599
+ if (found.has(unit.primary))
600
+ for (const file of unit.files)
601
+ found.add(file);
602
+ return found;
603
+ }
604
+ async function declaredEntries(root, declarations, packages, workspaces) {
605
+ const entries = [];
606
+ for (const file of declarations) {
607
+ const text = await readFile3(path4.join(root, file), "utf8");
608
+ const name = path4.posix.basename(file);
609
+ if (name === "package.json") {
610
+ const manifest = JSON.parse(text);
611
+ entries.push(...declaredBins(file, manifest), ...declaredScripts(file, manifest.scripts));
612
+ } else if (name === "angular.json")
613
+ entries.push(...angularEntries(file, text));
614
+ else if (name === "project.json")
615
+ entries.push(...nxEntries(file, text, workspaces));
616
+ else if (file.endsWith(".html"))
617
+ entries.push(...browserEntries(file, text, packages));
618
+ }
619
+ return entries;
620
+ }
621
+ function sourced(entry, files, outputs = []) {
622
+ const file = sourceOf(entry.file, outputs, files);
623
+ return file === undefined ? entry : { ...entry, file };
624
+ }
625
+ async function withJavaScriptEntries(root, observation, inputs, files) {
626
+ if (!observation)
627
+ return;
628
+ const inventory = files.filter((file) => ["package.json", "angular.json", "project.json", "nx.json"].includes(path4.posix.basename(file)) || file.endsWith(".html"));
629
+ const workspaces = new Set(inventory.filter((file) => path4.posix.basename(file) === "nx.json").map((file) => path4.posix.dirname(file)));
630
+ const packages = new Set(inventory.filter((file) => path4.posix.basename(file) === "package.json").map((file) => path4.posix.dirname(file)));
631
+ const active = new Set(observation.files.map((file) => packageFor(file.file, packages)));
632
+ const declarations = inventory.filter((file) => path4.posix.basename(file) !== "nx.json" && active.has(packageFor(file, packages)));
633
+ const entries = [...inputs.entries, ...await declaredEntries(root, declarations, packages, workspaces)];
634
+ const inventoryFiles = [...observation.files];
635
+ const visible = new Set(inventoryFiles.map((file) => file.file));
636
+ const readable = new Set(files);
637
+ const entryPoints = entries.map((entry) => sourced(entry, readable, inputs.buildOutputs)).flatMap(({ compiled = [], ...entry }) => {
638
+ if (!inputs.imports.has(entry.file))
639
+ return [];
640
+ const reached = reachedFiles(entry.file, compiled, inputs.imports, packages, observation.sourceUnits ?? []);
641
+ const members = observation.files.filter((file) => reached.has(file.file));
642
+ if (!members.length)
643
+ return [];
644
+ if (!visible.has(entry.file)) {
645
+ inventoryFiles.push({ file: entry.file, symbols: [], roots: [...new Set(members.flatMap((file) => file.roots))] });
646
+ visible.add(entry.file);
647
+ }
648
+ return [{ ...entry, files: [...new Set([entry.file, ...members.map((file) => file.file)])] }];
752
649
  });
650
+ return createScanObservation({ ...observation, files: inventoryFiles, entryPoints });
753
651
  }
754
652
 
755
- // plugins/scanners/react/src/http.ts
756
- import ts3 from "./typescript.cjs";
653
+ // plugins/scanners/entry-points/source.ts
654
+ import path5 from "path";
757
655
 
758
656
  // plugins/scanners/http-syntax.ts
759
657
  function wrapper(ts, node) {
@@ -775,8 +673,10 @@ function ownProperty(ts, object, name) {
775
673
  let found = "absent";
776
674
  for (const property of object.properties) {
777
675
  const key = propertyKey(ts, property);
778
- if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property))
779
- return "unknown";
676
+ if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
677
+ found = "unknown";
678
+ continue;
679
+ }
780
680
  if (key === name)
781
681
  found = ts.isPropertyAssignment(property) ? { node: property.initializer } : "unknown";
782
682
  }
@@ -1154,6 +1054,92 @@ function bindingUses(ts, checker, sources, options) {
1154
1054
  };
1155
1055
  }
1156
1056
 
1057
+ // plugins/scanners/http-url.ts
1058
+ var pathText2 = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
1059
+ var absolute = /^([A-Za-z][A-Za-z\d+.-]*:)?\/\//;
1060
+ var HOLE = "\x00";
1061
+ var computedPart = { kind: "hole", configured: false };
1062
+ var configuredPart = { kind: "hole", configured: true };
1063
+ var methodToken2 = /^[A-Z][A-Z-]*$/;
1064
+ function methodText(text) {
1065
+ const method = text?.toUpperCase();
1066
+ return method !== undefined && methodToken2.test(method) ? method : undefined;
1067
+ }
1068
+ function requestSegment2(part) {
1069
+ if (part === HOLE)
1070
+ return { kind: "dynamic" };
1071
+ if (part.includes(HOLE) || !pathText2.test(part))
1072
+ return { kind: "unknown" };
1073
+ return { kind: "literal", value: part };
1074
+ }
1075
+ function segmentsOf(parts) {
1076
+ const url = parts.map((part) => part.kind === "text" ? part.text : HOLE).join("");
1077
+ const end = Math.min(...["?", "#"].map((mark) => url.includes(mark) ? url.indexOf(mark) : url.length));
1078
+ return url.slice(0, end).split("/").filter((part) => part !== "").map(requestSegment2);
1079
+ }
1080
+ function afterAuthority(text) {
1081
+ const slash = text.indexOf("/", text.indexOf("//") + 2);
1082
+ return slash < 0 ? "" : text.slice(slash);
1083
+ }
1084
+ function joinedText(input) {
1085
+ const parts = [];
1086
+ for (const part of input) {
1087
+ const last = parts.at(-1);
1088
+ if (part.kind === "text" && last?.kind === "text")
1089
+ parts[parts.length - 1] = { kind: "text", text: last.text + part.text };
1090
+ else if (part.kind === "hole" || part.text !== "")
1091
+ parts.push(part);
1092
+ }
1093
+ return parts;
1094
+ }
1095
+ function requestUrl(input) {
1096
+ const parts = joinedText(input);
1097
+ const [first, ...rest] = parts;
1098
+ if (first === undefined)
1099
+ return { path: [{ kind: "unknown" }] };
1100
+ if (first.kind === "hole") {
1101
+ const path = segmentsOf(rest);
1102
+ const next = rest[0];
1103
+ const separated = next === undefined || next.kind === "text" && next.text.startsWith("/");
1104
+ return first.configured && separated ? { configured: true, path } : { path: [{ kind: "unknown" }, ...path] };
1105
+ }
1106
+ if (absolute.test(first.text)) {
1107
+ return { path: [{ kind: "unknown" }, ...segmentsOf([{ kind: "text", text: afterAuthority(first.text) }, ...rest])] };
1108
+ }
1109
+ if (!first.text.startsWith("/"))
1110
+ return { path: [{ kind: "unknown" }, ...segmentsOf(parts)] };
1111
+ return { path: segmentsOf(parts) };
1112
+ }
1113
+ function joinBase(base, url) {
1114
+ const stated = joinedText(base);
1115
+ const path = joinedText(url);
1116
+ const [first] = path;
1117
+ if (stated.length === 0)
1118
+ return path;
1119
+ if (first === undefined)
1120
+ return stated;
1121
+ if (first.kind === "hole")
1122
+ return [computedPart, ...path.slice(1)];
1123
+ return absolute.test(first.text) ? path : [...stated, { kind: "text", text: "/" }, ...path];
1124
+ }
1125
+ function withBase(base, url) {
1126
+ const stated = joinedText(base);
1127
+ const path = joinedText(url);
1128
+ const [only] = stated;
1129
+ const [first, ...rest] = path;
1130
+ if (stated.length !== 1 || only?.kind !== "text" || first?.kind !== "text" || absolute.test(first.text)) {
1131
+ return joinBase(stated, path);
1132
+ }
1133
+ const prefix = only.text.replace(/\/$/, "");
1134
+ if (first.text.startsWith(prefix)) {
1135
+ const next = first.text[prefix.length];
1136
+ if (next === undefined)
1137
+ return rest.length === 0 ? path : [computedPart, ...rest];
1138
+ return next === "/" || next === "?" ? path : joinBase(stated, path);
1139
+ }
1140
+ return prefix.startsWith(first.text) && rest.length > 0 ? [computedPart, ...rest] : joinBase(stated, path);
1141
+ }
1142
+
1157
1143
  // plugins/scanners/http-values.ts
1158
1144
  function urlContext(ts, checker, sources, fileAlone = false) {
1159
1145
  return { ts, checker, bindings: bindingUses(ts, checker, sources, { fileAlone }) };
@@ -1225,6 +1211,10 @@ async function variableHeld(context, name, path, depth) {
1225
1211
  return "unseen";
1226
1212
  if (path.length === 0 && ts.isFunctionDeclaration(declaration))
1227
1213
  return { node: declaration };
1214
+ if (path.length === 1 && ts.isEnumDeclaration(declaration)) {
1215
+ const member = declaration.members.find((entry) => ts.isIdentifier(entry.name) && entry.name.text === path[0]);
1216
+ return member?.initializer === undefined ? "unknown" : held(context, member.initializer, [], depth + 1);
1217
+ }
1228
1218
  if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined)
1229
1219
  return "unknown";
1230
1220
  if (!ts.isIdentifier(declaration.name) || !await unassigned(context, declaration))
@@ -1345,127 +1335,106 @@ async function literalText(context, node) {
1345
1335
  async function methodName(context, node) {
1346
1336
  return methodText(await literalText(context, node));
1347
1337
  }
1348
-
1349
- // plugins/scanners/http-clients.ts
1350
- async function declaredMethod(context, options, fallback) {
1351
- if (options === undefined)
1352
- return fallback;
1353
- const declared = await heldAt(context, options, "method");
1354
- if (declared === "absent")
1355
- return fallback;
1356
- return typeof declared === "string" ? undefined : methodName(context, declared.node);
1357
- }
1358
- async function optionsBase(context, config, fallback) {
1359
- if (config === undefined)
1360
- return fallback;
1361
- const base = await heldAt(context, config, "baseURL");
1362
- return base === "absent" ? fallback : heldParts(context, base);
1363
- }
1364
- async function runtimeFetch(context, callee) {
1365
- if (!context.ts.isIdentifier(callee) || callee.text !== "fetch")
1338
+ async function objectEntries(context, node) {
1339
+ const { ts } = context;
1340
+ const value = node === undefined ? undefined : await heldAt(context, node);
1341
+ if (typeof value !== "object" || !ts.isObjectLiteralExpression(value.node))
1366
1342
  return;
1367
- const origin = await importOrigin(context, callee);
1368
- const fetch = origin === undefined ? await runtimeGlobal(context, callee) : origin.module === "node-fetch" && origin.name === "default";
1369
- return fetch ? "fetch" : undefined;
1343
+ const entries = new Map;
1344
+ for (const property of value.node.properties) {
1345
+ const key = propertyKey(ts, property);
1346
+ if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property))
1347
+ return;
1348
+ if (ts.isPropertyAssignment(property))
1349
+ entries.set(key, property.initializer);
1350
+ else
1351
+ entries.delete(key);
1352
+ }
1353
+ return entries;
1370
1354
  }
1371
- async function fetchRequest(context, call, client) {
1372
- const [url, options] = call.arguments;
1373
- const kind = url === undefined ? undefined : await client(call.expression);
1374
- if (url === undefined || kind === undefined)
1355
+
1356
+ // plugins/scanners/entry-points/source.ts
1357
+ var relative = (root, file) => path5.relative(root, path5.resolve(root, file)).split(path5.sep).join("/");
1358
+ async function text2(context, node) {
1359
+ if (!node)
1375
1360
  return;
1376
- const input = await heldAt(context, url);
1377
- const method = input === "unseen" || typeof input === "object" && urlText(context.ts, input.node) ? await declaredMethod(context, options, "GET") : undefined;
1378
- const parts = await urlParts(context, url);
1379
- const target = kind === "ofetch" ? withBase(await optionsBase(context, options, []), parts) : parts;
1380
- return { ...method === undefined ? {} : { method }, ...requestUrl(target) };
1381
- }
1382
- async function withDefaults(context, client, holders) {
1383
- const found = await Promise.all(holders.map((holder) => context.bindings.settings(holder, ["defaults"], ["baseURL", "method"])));
1384
- const bases = found.flatMap((settings) => settings.assigned.get("baseURL") ?? []);
1385
- const methods = found.flatMap((settings) => settings.assigned.get("method") ?? []);
1386
- if (found.some((settings) => settings.changed) || bases.length > 1 || methods.length > 1)
1387
- return { base: [computedPart], method: undefined };
1388
- return {
1389
- base: bases[0] === undefined ? client.base : await urlParts(context, bases[0]),
1390
- method: methods[0] === undefined ? client.method : methodText(await literalText(context, methods[0]))
1391
- };
1392
- }
1393
- function defaultClient(context) {
1394
- return withDefaults(context, { base: [], method: "GET" }, context.bindings.defaultImports("axios"));
1395
- }
1396
- async function sent(context, client, url, method, config) {
1397
- const effective = method ?? await declaredMethod(context, config, client.method);
1398
- const base = await optionsBase(context, config, client.base);
1399
- return { ...effective === undefined ? {} : { method: effective }, ...requestUrl(joinBase(base, url)) };
1400
- }
1401
- async function configRequest(context, client, config) {
1402
- const url = config === undefined ? "absent" : await heldAt(context, config, "url");
1403
- return url === "absent" ? undefined : sent(context, client, await heldParts(context, url), undefined, config);
1404
- }
1405
- async function holdsObject(context, node) {
1406
- const value = await heldAt(context, node);
1407
- return typeof value === "object" && context.ts.isObjectLiteralExpression(value.node);
1408
- }
1409
- var SHORTHAND = new Map([
1410
- ["get", "GET"],
1411
- ["post", "POST"],
1412
- ["put", "PUT"],
1413
- ["patch", "PATCH"],
1414
- ["delete", "DELETE"],
1415
- ["head", "HEAD"],
1416
- ["options", "OPTIONS"]
1417
- ]);
1418
- var WITH_BODY = new Set(["post", "put", "patch"]);
1419
- async function isAxios(context, node) {
1420
- const origin = await importOrigin(context, node);
1421
- return origin?.module === "axios" && origin.name === "default";
1361
+ const parts = await urlParts(context, node);
1362
+ return parts.every((part) => part.kind === "text") ? parts.map((part) => part.kind === "text" ? part.text : "").join("") : undefined;
1422
1363
  }
1423
- async function createdClient(context, node) {
1364
+ async function entryPath(root, context, node) {
1424
1365
  const { ts } = context;
1425
- const created = await heldAt(context, node);
1426
- const call = typeof created === "object" && ts.isCallExpression(created.node) ? created.node : undefined;
1427
- const callee = call?.expression;
1428
- if (call === undefined || callee === undefined || !ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name))
1366
+ const direct = await text2(context, node);
1367
+ if (direct !== undefined)
1368
+ return relative(root, direct);
1369
+ if (!ts.isCallExpression(node))
1429
1370
  return;
1430
- if (callee.name.text !== "create" || !await isAxios(context, callee.expression))
1371
+ const origin = await importOrigin(context, node.expression);
1372
+ if (origin?.name !== "fileURLToPath" || !["node:url", "url"].includes(origin.module))
1431
1373
  return;
1432
- const parent = await defaultClient(context);
1433
- const inherited = { base: parent.base.length === 0 ? [] : [computedPart], method: parent.method === "GET" ? "GET" : undefined };
1434
- const [config] = call.arguments;
1435
- const client = { base: await optionsBase(context, config, inherited.base), method: await declaredMethod(context, config, inherited.method) };
1436
- const holders = [await declarationOf(context, node), holderOf(ts, call)].filter((holder) => holder !== undefined);
1437
- return withDefaults(context, client, [...new Set(holders)]);
1438
- }
1439
- async function axiosClient(context, node) {
1440
- if (!context.ts.isIdentifier(node))
1374
+ const url = node.arguments[0];
1375
+ if (!url || !ts.isNewExpression(url) || !ts.isIdentifier(url.expression) || url.expression.text !== "URL" || !await runtimeGlobal(context, url.expression))
1441
1376
  return;
1442
- return await isAxios(context, node) ? defaultClient(context) : createdClient(context, node);
1377
+ const base = url.arguments?.[1];
1378
+ if (!base || !ts.isPropertyAccessExpression(base) || base.name.text !== "url" || !ts.isMetaProperty(base.expression))
1379
+ return;
1380
+ const value = await text2(context, url.arguments?.[0]);
1381
+ return value === undefined ? undefined : relative(root, path5.resolve(root, path5.dirname(node.getSourceFile().fileName), value));
1443
1382
  }
1444
- async function axiosRequest(context, call) {
1383
+ async function buildEntries(root, context, call) {
1445
1384
  const { ts } = context;
1446
1385
  const callee = call.expression;
1447
- const [first, second, third] = call.arguments;
1448
- if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
1449
- const client = await axiosClient(context, callee.expression);
1450
- if (client === undefined)
1451
- return;
1452
- const member = callee.name.text;
1453
- const method = SHORTHAND.get(member);
1454
- if (method !== undefined && first !== undefined) {
1455
- return sent(context, client, await urlParts(context, first), method, WITH_BODY.has(member) ? third : second);
1456
- }
1457
- return member === "request" ? configRequest(context, client, first) : undefined;
1458
- }
1459
- const client = await axiosClient(context, callee);
1460
- if (client === undefined || first === undefined)
1461
- return;
1462
- if (await holdsObject(context, first))
1463
- return configRequest(context, client, first);
1464
- return sent(context, client, await urlParts(context, first), undefined, second);
1386
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "build" || !ts.isIdentifier(callee.expression) || callee.expression.text !== "Bun" || !await runtimeGlobal(context, callee.expression))
1387
+ return [];
1388
+ const fields = await objectEntries(context, call.arguments[0]);
1389
+ if (!fields)
1390
+ return [];
1391
+ const compile = fields.get("compile"), entrypoints = fields.get("entrypoints");
1392
+ const executable = compile && await heldAt(context, compile);
1393
+ const native = typeof executable === "object" && (executable.node.kind === ts.SyntaxKind.TrueKeyword || ts.isObjectLiteralExpression(executable.node));
1394
+ if (!native && await literalText(context, fields.get("target")) !== "browser")
1395
+ return [];
1396
+ const array = entrypoints && await heldAt(context, entrypoints);
1397
+ if (typeof array !== "object" || !ts.isArrayLiteralExpression(array.node))
1398
+ return [];
1399
+ const declaration = relative(root, call.getSourceFile().fileName);
1400
+ const files = await Promise.all(array.node.elements.map((node) => entryPath(root, context, node)));
1401
+ return files.flatMap((file) => file === undefined ? [] : [{ file, declaration, name: path5.posix.basename(file).replace(/\.[^.]+$/, "") }]);
1465
1402
  }
1466
-
1467
- // plugins/scanners/http-checker.ts
1468
- function primitive(ts, type) {
1403
+ async function sourceBuildEntries(root, ts, checker, sources) {
1404
+ const context = urlContext(ts, checker, sources);
1405
+ const calls = [];
1406
+ const visit = (node) => {
1407
+ if (ts.isCallExpression(node))
1408
+ calls.push(node);
1409
+ ts.forEachChild(node, visit);
1410
+ };
1411
+ for (const source of sources)
1412
+ visit(source);
1413
+ return (await Promise.all(calls.map((call) => buildEntries(root, context, call)))).flat();
1414
+ }
1415
+ async function entrySourceInputs(root, ts, checker, sources) {
1416
+ const selected = new Set(sources.map((source) => relative(root, source.fileName)));
1417
+ const imports = new Map;
1418
+ for (const source of sources) {
1419
+ const literals = [];
1420
+ const visit = (node) => {
1421
+ if (ts.isStringLiteral(node))
1422
+ literals.push(node);
1423
+ ts.forEachChild(node, visit);
1424
+ };
1425
+ visit(source);
1426
+ const symbols = await checker.symbolsAt(literals);
1427
+ const declarations = await Promise.all(symbols.flatMap((symbol) => symbol && checker.module(symbol) ? [checker.declaration(symbol)] : []));
1428
+ imports.set(relative(root, source.fileName), [...new Set(declarations.flatMap((declaration) => {
1429
+ const file = declaration && relative(root, declaration.getSourceFile().fileName);
1430
+ return file && selected.has(file) ? [file] : [];
1431
+ }))]);
1432
+ }
1433
+ return { imports, entries: await sourceBuildEntries(root, ts, checker, sources) };
1434
+ }
1435
+
1436
+ // plugins/scanners/http-checker.ts
1437
+ function primitive(ts, type) {
1469
1438
  if (type.isUnion())
1470
1439
  return (type.types ?? []).every((member) => primitive(ts, member));
1471
1440
  const flags = ts.TypeFlags;
@@ -1487,74 +1456,278 @@ function classicChecker(ts, checker) {
1487
1456
  };
1488
1457
  }
1489
1458
 
1490
- // plugins/scanners/react/src/http.ts
1491
- async function reactHttpRequests(sources, analyzed, checker, callerOperation) {
1492
- const context = urlContext(ts3, classicChecker(ts3, checker), analyzed);
1493
- const calls = [];
1494
- const visit = (node) => {
1495
- if (ts3.isCallExpression(node))
1496
- calls.push(node);
1497
- ts3.forEachChild(node, visit);
1498
- };
1499
- for (const source of sources)
1500
- visit(source);
1501
- const requests = [];
1502
- for (const call of calls) {
1503
- const request = await fetchRequest(context, call, (callee) => runtimeFetch(context, callee)) ?? await axiosRequest(context, call);
1504
- const operation = request === undefined ? undefined : callerOperation(call);
1505
- if (request !== undefined && operation !== undefined)
1506
- requests.push({ operation, ...request });
1459
+ // plugins/scanners/react/src/scan.ts
1460
+ import ts6 from "./typescript.cjs";
1461
+
1462
+ // plugins/scanners/observations.ts
1463
+ function combineObservations(parts) {
1464
+ if (!parts.length)
1465
+ return;
1466
+ const roots = [];
1467
+ const files = new Map;
1468
+ const operations = [];
1469
+ const invocations = [];
1470
+ const diagnostics = [];
1471
+ const sourceUnits = [];
1472
+ const entryPoints = [];
1473
+ const httpEndpoints = [];
1474
+ const httpRequests = [];
1475
+ for (const { key, observation } of parts) {
1476
+ const id = (value) => JSON.stringify([key, value]);
1477
+ roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
1478
+ for (const source of observation.files) {
1479
+ const combined = files.get(source.file) ?? { file: source.file, roots: [], symbols: [] };
1480
+ combined.roots.push(...source.roots.map(id));
1481
+ combined.symbols.push(...source.symbols.map((symbol) => ({ ...symbol, id: id(symbol.id) })));
1482
+ files.set(source.file, combined);
1483
+ }
1484
+ operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
1485
+ invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
1486
+ httpEndpoints.push(...(observation.httpEndpoints ?? []).map((endpoint) => ({ ...endpoint, operation: id(endpoint.operation) })));
1487
+ httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
1488
+ diagnostics.push(...observation.diagnostics);
1489
+ sourceUnits.push(...observation.sourceUnits ?? []);
1490
+ entryPoints.push(...observation.entryPoints ?? []);
1507
1491
  }
1508
- return requests;
1492
+ return createScanObservation({
1493
+ scanner: parts[0].observation.scanner,
1494
+ roots,
1495
+ files: [...files.values()],
1496
+ operations,
1497
+ invocations,
1498
+ diagnostics,
1499
+ ...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
1500
+ ...parts.some((part) => part.observation.entryPoints !== undefined) ? { entryPoints } : {},
1501
+ ...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
1502
+ ...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
1503
+ });
1509
1504
  }
1510
1505
 
1511
- // plugins/scanners/react/src/scan.ts
1512
- function relative(root, file) {
1513
- return path5.relative(root, file).split(path5.sep).join("/");
1506
+ // plugins/scanners/react/src/evidence.ts
1507
+ import ts4 from "./typescript.cjs";
1508
+
1509
+ // plugins/scanners/react/src/functions.ts
1510
+ import ts from "./typescript.cjs";
1511
+ function executable(node) {
1512
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
1513
+ return true;
1514
+ return (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) && node.body !== undefined;
1515
+ }
1516
+ function caller(node) {
1517
+ for (let parent = node.parent;parent; parent = parent.parent)
1518
+ if (executable(parent))
1519
+ return parent;
1520
+ return;
1521
+ }
1522
+ function operationName(node) {
1523
+ const name = node.name;
1524
+ if (name !== undefined && !ts.isComputedPropertyName(name))
1525
+ return name.text;
1526
+ return ts.isVariableDeclaration(node.parent) ? node.parent.name.getText() : "callback";
1527
+ }
1528
+
1529
+ // plugins/scanners/react/src/project.ts
1530
+ import { readFileSync } from "fs";
1531
+ import path7 from "path";
1532
+ import ts3 from "./typescript.cjs";
1533
+
1534
+ // plugins/scanners/projects.ts
1535
+ function isUnder(file, directory) {
1536
+ return directory === "" || file.startsWith(`${directory}/`);
1537
+ }
1538
+
1539
+ // plugins/scanners/react/src/routes.ts
1540
+ import { existsSync } from "fs";
1541
+ import path6 from "path";
1542
+ import ts2 from "./typescript.cjs";
1543
+ var METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
1544
+ var APP_ROUTE = /^(?:(.+)\/)?route\.tsx?$/;
1545
+ var PAGES_API = /^(api\/.+)\.tsx?$/;
1546
+ var OPTIONAL_CATCH_ALL = /^\[\[\.\.\.(.+)\]\]$/;
1547
+ var CATCH_ALL = /^\[\.\.\.(.+)\]$/;
1548
+ var PARAMETER = /^\[(.+)\]$/;
1549
+ function nextRouters(directory) {
1550
+ const active = (name) => existsSync(path6.join(directory, name)) ? name : `src/${name}`;
1551
+ return { app: active("app"), pages: active("pages") };
1552
+ }
1553
+ function routeLocation(file, routers) {
1554
+ const within = (directory) => file.startsWith(`${directory}/`) ? file.slice(directory.length + 1) : undefined;
1555
+ const app = APP_ROUTE.exec(within(routers.app) ?? "");
1556
+ if (app)
1557
+ return app[1]?.split("/").some((part) => part.startsWith("_")) ? undefined : { router: "app", ...app[1] === undefined ? {} : { directory: app[1] } };
1558
+ const pages = PAGES_API.exec(within(routers.pages) ?? "");
1559
+ return pages ? { router: "pages", directory: pages[1] } : undefined;
1560
+ }
1561
+ var GROUP = /^\((?!\.+\))[^)]+\)$/;
1562
+ function routeSegment(part, last) {
1563
+ const optional = OPTIONAL_CATCH_ALL.exec(part);
1564
+ if (optional)
1565
+ return last && pathText2.test(optional[1]) ? { kind: "catch-all", name: optional[1], optional: true } : undefined;
1566
+ const rest = CATCH_ALL.exec(part);
1567
+ if (rest)
1568
+ return last && pathText2.test(rest[1]) ? { kind: "catch-all", name: rest[1] } : undefined;
1569
+ const parameter = PARAMETER.exec(part);
1570
+ if (parameter)
1571
+ return pathText2.test(parameter[1]) ? { kind: "parameter", name: parameter[1] } : undefined;
1572
+ if (part.startsWith("(") || part.startsWith("@") || part.includes("[") || part.includes("]"))
1573
+ return;
1574
+ return pathText2.test(part) ? { kind: "literal", value: part } : undefined;
1575
+ }
1576
+ function routeParts(directory, stripIndex) {
1577
+ const parts = (directory ?? "").split("/").filter((part) => part !== "" && !GROUP.test(part));
1578
+ return stripIndex && parts.at(-1) === "index" ? parts.slice(0, -1) : parts;
1579
+ }
1580
+ function routePath(directory, stripIndex) {
1581
+ const parts = routeParts(directory, stripIndex);
1582
+ const segments = [];
1583
+ for (const [index, part] of parts.entries()) {
1584
+ const segment = routeSegment(part, index === parts.length - 1);
1585
+ if (segment === undefined)
1586
+ return;
1587
+ segments.push(segment);
1588
+ }
1589
+ return segments;
1590
+ }
1591
+ function hasModifier2(statement, kind) {
1592
+ return ts2.canHaveModifiers(statement) && (ts2.getModifiers(statement) ?? []).some((modifier) => modifier.kind === kind);
1593
+ }
1594
+ function literal(node) {
1595
+ return ts2.isObjectLiteralExpression(node) || ts2.isArrayLiteralExpression(node) || ts2.isLiteralExpression(node) || node.kind === ts2.SyntaxKind.TrueKeyword || node.kind === ts2.SyntaxKind.FalseKeyword || node.kind === ts2.SyntaxKind.NullKeyword;
1596
+ }
1597
+ function served(source, value, depth = 0) {
1598
+ if (value === undefined || literal(value))
1599
+ return;
1600
+ if (executable(value))
1601
+ return value;
1602
+ return ts2.isIdentifier(value) && depth < 8 ? servedName(source, value.text, depth + 1) : source;
1603
+ }
1604
+ function servedName(source, name, depth = 0) {
1605
+ for (const statement of source.statements) {
1606
+ if (ts2.isFunctionDeclaration(statement) && statement.name?.text === name)
1607
+ return served(source, statement, depth);
1608
+ if (!ts2.isVariableStatement(statement))
1609
+ continue;
1610
+ for (const declaration of statement.declarationList.declarations) {
1611
+ if (ts2.isIdentifier(declaration.name) && declaration.name.text === name)
1612
+ return served(source, declaration.initializer, depth);
1613
+ }
1614
+ }
1615
+ return source;
1616
+ }
1617
+ function statementExports(source, statement) {
1618
+ if (ts2.isExportDeclaration(statement)) {
1619
+ if (statement.isTypeOnly || statement.exportClause === undefined || !ts2.isNamedExports(statement.exportClause))
1620
+ return [];
1621
+ return statement.exportClause.elements.filter((element) => !element.isTypeOnly).map((element) => [
1622
+ element.name.text,
1623
+ statement.moduleSpecifier === undefined ? servedName(source, (element.propertyName ?? element.name).text) : source
1624
+ ]);
1625
+ }
1626
+ if (!hasModifier2(statement, ts2.SyntaxKind.ExportKeyword))
1627
+ return [];
1628
+ const name = hasModifier2(statement, ts2.SyntaxKind.DefaultKeyword) ? "default" : undefined;
1629
+ if (ts2.isFunctionDeclaration(statement))
1630
+ return [[name ?? statement.name.text, served(source, statement)]];
1631
+ if (!ts2.isVariableStatement(statement))
1632
+ return [];
1633
+ return statement.declarationList.declarations.flatMap((declaration) => ts2.isIdentifier(declaration.name) ? [[declaration.name.text, served(source, declaration.initializer)]] : ts2.isObjectBindingPattern(declaration.name) ? declaration.name.elements.flatMap((element) => ts2.isIdentifier(element.name) ? [[element.name.text, source]] : []) : []);
1634
+ }
1635
+ function routeExports(source) {
1636
+ return new Map(source.statements.flatMap((statement) => ts2.isExportAssignment(statement) ? statement.isExportEquals === true ? [] : [["default", served(source, statement.expression)]] : statementExports(source, statement)));
1637
+ }
1638
+ function answeredMethod(router, name) {
1639
+ if (router === "app")
1640
+ return METHODS.has(name) ? name : undefined;
1641
+ return name === "default" ? "*" : undefined;
1642
+ }
1643
+ function nextRouteEndpoints(sources, directory, routers, operationId) {
1644
+ const endpoints = [];
1645
+ for (const source of sources) {
1646
+ const location = routeLocation(path6.relative(directory, source.fileName).split(path6.sep).join("/"), routers);
1647
+ const segments = location === undefined ? undefined : routePath(location.directory, location.router === "pages");
1648
+ if (location === undefined || segments === undefined)
1649
+ continue;
1650
+ for (const [name, handler] of routeExports(source)) {
1651
+ const method = answeredMethod(location.router, name);
1652
+ if (method !== undefined && handler !== undefined)
1653
+ endpoints.push({ operation: operationId(handler), method, path: segments });
1654
+ }
1655
+ }
1656
+ return endpoints;
1657
+ }
1658
+
1659
+ // plugins/scanners/react/src/project.ts
1660
+ var absentBase = new Set([5083, 6053]);
1661
+ function relative2(root, file) {
1662
+ return path7.relative(root, file).split(path7.sep).join("/");
1514
1663
  }
1515
1664
  function failDiagnostics(diagnostics) {
1516
- const errors = diagnostics.filter((item) => item.category === ts4.DiagnosticCategory.Error);
1665
+ const errors = diagnostics.filter((item) => item.category === ts3.DiagnosticCategory.Error);
1517
1666
  if (errors.length)
1518
- throw new Error(errors.map((item) => `${item.file?.fileName ?? "React"}: ${ts4.flattenDiagnosticMessageText(item.messageText, `
1667
+ throw new Error(errors.map((item) => `${item.file?.fileName ?? "React"}: ${ts3.flattenDiagnosticMessageText(item.messageText, `
1519
1668
  `)}`).join(`
1520
1669
  `));
1521
1670
  }
1522
- function reactProject(root, repositoryRoot = root) {
1523
- const manifestFile = path5.join(root, "package.json");
1524
- if (!existsSync3(manifestFile))
1525
- return;
1526
- const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
1527
- if (!hasDependency(manifest, "react"))
1528
- return;
1529
- try {
1530
- const configFile = path5.join(root, "tsconfig.json");
1531
- const config = ts4.readConfigFile(configFile, ts4.sys.readFile);
1532
- if (config.error)
1533
- failDiagnostics([config.error]);
1534
- const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, root, undefined, configFile);
1535
- failDiagnostics(parsed.errors);
1536
- const program = ts4.createProgram(parsed.fileNames, { ...parsed.options, noEmit: true });
1537
- const owned = program.getSourceFiles().filter((source) => {
1538
- const file = relative(repositoryRoot, source.fileName);
1539
- return !source.isDeclarationFile && !file.startsWith("../") && !file.includes("node_modules/");
1540
- });
1541
- const sources = owned.filter((source) => source.fileName.endsWith(".tsx"));
1542
- if (!sources.length)
1543
- throw new Error("The project tsconfig.json must include React TSX source files.");
1544
- const routers = nextRouters(root);
1545
- const routes = hasDependency(manifest, "next") ? owned.filter((source) => routeLocation(relative(root, source.fileName), routers) !== undefined) : [];
1546
- failDiagnostics(program.getSyntacticDiagnostics());
1547
- return { manifest, program, owned, sources, routes, routers };
1548
- } catch (error) {
1549
- throw new Error(`REACT_SOURCE_INVALID: Check the project tsconfig.json and TSX syntax. ${error}`);
1671
+ function packageConfig(directory, repositoryRoot, configs) {
1672
+ for (let current = directory;; current = path7.dirname(current)) {
1673
+ const file = path7.join(current, "tsconfig.json");
1674
+ if (configs.has(relative2(repositoryRoot, file)))
1675
+ return file;
1676
+ if (current === repositoryRoot)
1677
+ throw new Error(`${directory}: no TypeScript config in the package or its repository ancestors`);
1550
1678
  }
1551
1679
  }
1552
- function caller(node) {
1553
- for (let parent = node.parent;parent; parent = parent.parent)
1554
- if (executable(parent))
1555
- return parent;
1680
+ function componentConfig(configFile, repositoryRoot, typescriptSources, diagnostics) {
1681
+ const config = ts3.readConfigFile(configFile, ts3.sys.readFile);
1682
+ if (config.error)
1683
+ failDiagnostics([config.error]);
1684
+ const parsed = ts3.parseJsonConfigFileContent(config.config, ts3.sys, path7.dirname(configFile), undefined, configFile);
1685
+ failDiagnostics(parsed.errors.filter((item) => !absentBase.has(item.code)));
1686
+ diagnostics.push(...parsed.errors.filter((item) => absentBase.has(item.code)).map((item) => ({
1687
+ severity: "warning",
1688
+ code: "react-missing-config-base",
1689
+ file: relative2(repositoryRoot, configFile),
1690
+ message: `The extended TypeScript config is absent; source uses the available settings. ${ts3.flattenDiagnosticMessageText(item.messageText, " ")}`
1691
+ })));
1692
+ if (parsed.fileNames.length || !parsed.projectReferences?.length)
1693
+ return parsed;
1694
+ for (const reference of parsed.projectReferences) {
1695
+ const referenced = componentConfig(ts3.resolveProjectReferencePath(reference), repositoryRoot, typescriptSources, diagnostics);
1696
+ if (referenced?.fileNames.some((file) => file.endsWith(".tsx") && typescriptSources.has(relative2(repositoryRoot, file))))
1697
+ return referenced;
1698
+ }
1556
1699
  return;
1557
1700
  }
1701
+ function reactProject(directory, repositoryRoot, typescriptSources, configs, packages) {
1702
+ const manifest = JSON.parse(readFileSync(path7.join(directory, "package.json"), "utf8"));
1703
+ const configFile = packageConfig(directory, repositoryRoot, configs);
1704
+ try {
1705
+ const project = relative2(repositoryRoot, directory);
1706
+ const owner = (file) => packages.find((item) => isUnder(file, item)) ?? project;
1707
+ const belongs = (file) => owner(file) === project;
1708
+ const diagnostics = [];
1709
+ const config = componentConfig(configFile, repositoryRoot, typescriptSources, diagnostics);
1710
+ if (config === undefined)
1711
+ return;
1712
+ const routers = nextRouters(directory);
1713
+ const routeFiles = hasDependency(manifest, "next") ? [...typescriptSources].filter((file) => isUnder(file, project) && belongs(file) && routeLocation(project === "" ? file : file.slice(project.length + 1), routers) !== undefined) : [];
1714
+ const roots = [...new Set([...config.fileNames, ...routeFiles.map((file) => path7.join(repositoryRoot, file))])];
1715
+ const program = ts3.createProgram(roots, { ...config.options, noEmit: true });
1716
+ const readable = program.getSourceFiles().filter((source) => typescriptSources.has(relative2(repositoryRoot, source.fileName)));
1717
+ const owned = readable.filter((source) => belongs(relative2(repositoryRoot, source.fileName)));
1718
+ for (const source of owned)
1719
+ failDiagnostics(program.getSyntacticDiagnostics(source));
1720
+ const components = owned.filter((source) => source.fileName.endsWith(".tsx"));
1721
+ if (!components.length)
1722
+ return;
1723
+ const routes = owned.filter((source) => routeFiles.includes(relative2(repositoryRoot, source.fileName)));
1724
+ return { manifest, program, routers, readable, owned, components, routes, diagnostics, owner };
1725
+ } catch (error) {
1726
+ throw new Error(`REACT_SOURCE_INVALID: Check ${relative2(repositoryRoot, configFile)} and its TSX syntax. ${error}`);
1727
+ }
1728
+ }
1729
+
1730
+ // plugins/scanners/react/src/evidence.ts
1558
1731
  function writesSymbol(target, symbol, checker) {
1559
1732
  if (ts4.isIdentifier(target))
1560
1733
  return checker.getSymbolAtLocation(target) === symbol;
@@ -1574,6 +1747,14 @@ function writesSymbol(target, symbol, checker) {
1574
1747
  }
1575
1748
  return false;
1576
1749
  }
1750
+ function assignment2(node) {
1751
+ return ts4.isBinaryExpression(node) && node.operatorToken.kind >= ts4.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts4.SyntaxKind.LastAssignment;
1752
+ }
1753
+ var passThrough = new Set(["useCallback", "memo", "forwardRef"]);
1754
+ function platform(file, suffixes) {
1755
+ const stem = file.replace(/\.[cm]?[jt]sx?$/, "");
1756
+ return suffixes.find((suffix) => stem.endsWith(suffix)) ?? "";
1757
+ }
1577
1758
 
1578
1759
  class Evidence {
1579
1760
  operations = new Map;
@@ -1582,20 +1763,43 @@ class Evidence {
1582
1763
  root;
1583
1764
  checker;
1584
1765
  files;
1585
- constructor(root, checker, sources) {
1766
+ platforms;
1767
+ constructor(root, program, readable) {
1586
1768
  this.root = root;
1587
- this.checker = checker;
1588
- this.files = new Set(sources);
1769
+ this.checker = program.getTypeChecker();
1770
+ this.files = new Set(readable);
1771
+ const suffixes = (program.getCompilerOptions().moduleSuffixes ?? []).filter((suffix) => suffix !== "");
1772
+ this.platforms = suffixes.length ? [...suffixes, ".web"] : [];
1589
1773
  }
1590
1774
  operationId(node) {
1591
- const file = relative(this.root, node.getSourceFile().fileName);
1775
+ const file = relative2(this.root, node.getSourceFile().fileName);
1592
1776
  const position = node.getStart();
1593
- const id = `${file}#${position}`;
1594
- const name = node.name?.text ?? (ts4.isVariableDeclaration(node.parent) ? node.parent.name.getText() : "callback");
1595
- this.operations.set(id, { id, file, name, position });
1777
+ const module = ts4.isSourceFile(node);
1778
+ const id = `${file}#${module ? "module" : position}`;
1779
+ this.operations.set(id, { id, file, name: module ? "(anonymous)" : operationName(node), position });
1596
1780
  return id;
1597
1781
  }
1598
- functionValue(name) {
1782
+ reactPassThrough(callee) {
1783
+ const member = ts4.isPropertyAccessExpression(callee) ? callee.name.text : undefined;
1784
+ const local = ts4.isPropertyAccessExpression(callee) ? callee.expression : callee;
1785
+ const declaration = ts4.isIdentifier(local) ? this.checker.getSymbolAtLocation(local)?.declarations?.[0] : undefined;
1786
+ const statement = declaration && ts4.findAncestor(declaration, ts4.isImportDeclaration);
1787
+ if (!statement || !ts4.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react")
1788
+ return false;
1789
+ const imported = ts4.isImportSpecifier(declaration) ? (declaration.propertyName ?? declaration.name).text : undefined;
1790
+ return member === undefined ? imported !== undefined && passThrough.has(imported) : imported === undefined && passThrough.has(member);
1791
+ }
1792
+ unwrapped(node, depth) {
1793
+ if (executable(node))
1794
+ return node;
1795
+ if (!ts4.isCallExpression(node) || depth > 4 || !this.reactPassThrough(node.expression))
1796
+ return;
1797
+ const [value] = node.arguments;
1798
+ if (value === undefined)
1799
+ return;
1800
+ return ts4.isIdentifier(value) ? this.functionValue(value, depth + 1) : this.unwrapped(value, depth + 1);
1801
+ }
1802
+ functionValue(name, depth = 0) {
1599
1803
  let symbol = this.checker.getSymbolAtLocation(name);
1600
1804
  if (symbol && symbol.flags & ts4.SymbolFlags.Alias)
1601
1805
  symbol = this.checker.getAliasedSymbol(symbol);
@@ -1604,56 +1808,77 @@ class Evidence {
1604
1808
  return;
1605
1809
  if (executable(declaration))
1606
1810
  return declaration;
1607
- if (ts4.isVariableDeclaration(declaration) && declaration.parent.flags & ts4.NodeFlags.Const && declaration.initializer && executable(declaration.initializer))
1608
- return declaration.initializer;
1811
+ if (ts4.isVariableDeclaration(declaration) && declaration.parent.flags & ts4.NodeFlags.Const && declaration.initializer)
1812
+ return this.unwrapped(declaration.initializer, depth);
1609
1813
  return;
1610
1814
  }
1611
- unsupported(node) {
1815
+ sameBuild(user, used) {
1816
+ const own = platform(user.fileName, this.platforms);
1817
+ if (own === "")
1818
+ return true;
1819
+ const theirs = platform(used.fileName, this.platforms);
1820
+ if (theirs !== "")
1821
+ return theirs === own;
1822
+ return !ts4.sys.fileExists(used.fileName.replace(/(\.[cm]?[jt]sx?)$/, `${own}$1`));
1823
+ }
1824
+ unsupported(node, reason) {
1612
1825
  const source = node.getSourceFile();
1613
1826
  this.diagnostics.push({
1614
1827
  severity: "info",
1615
1828
  code: "unsupported-react-binding",
1616
- file: relative(this.root, source.fileName),
1829
+ file: relative2(this.root, source.fileName),
1617
1830
  line: source.getLineAndCharacterOfPosition(node.getStart()).line + 1,
1618
- message: "No supported direct component prop-to-handler binding."
1831
+ message: `No supported callback binding: ${reason}.`
1619
1832
  });
1620
1833
  }
1621
- propCalls(component, attribute) {
1622
- const parameter = component.parameters[0];
1623
- if (!parameter || !ts4.isObjectBindingPattern(parameter.name))
1624
- return [];
1625
- const binding = parameter.name.elements.find((element) => !element.dotDotDotToken && !element.initializer && (element.propertyName ?? element.name).getText() === attribute.name.getText());
1626
- if (!binding || !ts4.isIdentifier(binding.name))
1627
- return [];
1628
- const symbol = this.checker.getSymbolAtLocation(binding.name);
1629
- if (!symbol)
1630
- return [];
1834
+ calls(component, symbol, callee) {
1631
1835
  const calls = [];
1632
1836
  let reassigned = false;
1633
- const checker = this.checker;
1634
1837
  const visit = (node) => {
1635
- if (ts4.isBinaryExpression(node) && node.operatorToken.kind >= ts4.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts4.SyntaxKind.LastAssignment && writesSymbol(node.left, symbol, checker))
1838
+ if (assignment2(node) && (writesSymbol(node.left, symbol, this.checker) || callee(node.left)))
1636
1839
  reassigned = true;
1637
- if (ts4.isForOfStatement(node) && writesSymbol(node.initializer, symbol, checker))
1840
+ if (ts4.isForOfStatement(node) && writesSymbol(node.initializer, symbol, this.checker))
1638
1841
  reassigned = true;
1639
- if (ts4.isCallExpression(node) && ts4.isIdentifier(node.expression) && checker.getSymbolAtLocation(node.expression) === symbol)
1842
+ if (ts4.isCallExpression(node) && callee(node.expression))
1640
1843
  calls.push(node);
1641
1844
  ts4.forEachChild(node, visit);
1642
1845
  };
1643
1846
  visit(component);
1644
- return reassigned ? [] : calls;
1847
+ return reassigned ? undefined : calls;
1848
+ }
1849
+ propCalls(component, prop) {
1850
+ const parameter = component.parameters[0];
1851
+ const named = (name) => prop === undefined || name === prop;
1852
+ if (parameter !== undefined && ts4.isIdentifier(parameter.name)) {
1853
+ const props = this.checker.getSymbolAtLocation(parameter.name);
1854
+ return props === undefined ? [] : this.calls(component, props, (node) => ts4.isPropertyAccessExpression(node) && named(node.name.text) && ts4.isIdentifier(node.expression) && this.checker.getSymbolAtLocation(node.expression) === props);
1855
+ }
1856
+ if (parameter === undefined || !ts4.isObjectBindingPattern(parameter.name))
1857
+ return [];
1858
+ const found = parameter.name.elements.filter((element) => !element.dotDotDotToken && named((element.propertyName ?? element.name).getText())).map((element) => {
1859
+ const symbol = ts4.isIdentifier(element.name) ? this.checker.getSymbolAtLocation(element.name) : undefined;
1860
+ return symbol === undefined ? [] : this.calls(component, symbol, (node) => ts4.isIdentifier(node) && this.checker.getSymbolAtLocation(node) === symbol);
1861
+ });
1862
+ if (found.includes(undefined))
1863
+ return;
1864
+ return found.flatMap((item) => item ?? []);
1645
1865
  }
1646
1866
  binding(component, attribute) {
1647
1867
  const initializer = attribute.initializer;
1648
1868
  if (!initializer || !ts4.isJsxExpression(initializer) || !initializer.expression)
1649
1869
  return;
1650
- const expression = initializer.expression;
1651
- if (!this.checker.getTypeAtLocation(expression).getCallSignatures().length)
1870
+ const prop = attribute.name.getText();
1871
+ const calls = this.propCalls(component, prop);
1872
+ if (calls?.length === 0)
1873
+ return;
1874
+ if (calls === undefined) {
1875
+ this.unsupported(attribute, `the component writes ${prop}`);
1652
1876
  return;
1653
- const target = ts4.isIdentifier(expression) ? this.functionValue(expression) : undefined;
1654
- const calls = this.propCalls(component, attribute);
1655
- if (!target || !calls.length) {
1656
- this.unsupported(attribute);
1877
+ }
1878
+ const expression = initializer.expression;
1879
+ const target = executable(expression) ? expression : ts4.isIdentifier(expression) ? this.functionValue(expression) : undefined;
1880
+ if (!target) {
1881
+ this.unsupported(attribute, `${prop} receives a value that is not a function the sources define`);
1657
1882
  return;
1658
1883
  }
1659
1884
  const source = attribute.getSourceFile();
@@ -1665,11 +1890,11 @@ class Evidence {
1665
1890
  source: this.operationId(operation),
1666
1891
  targets: [this.operationId(target)],
1667
1892
  unresolved: false,
1668
- member: attribute.name.getText(),
1893
+ member: prop,
1669
1894
  position: call.getStart(),
1670
1895
  line: call.getSourceFile().getLineAndCharacterOfPosition(call.getStart()).line + 1,
1671
1896
  binding: {
1672
- file: relative(this.root, source.fileName),
1897
+ file: relative2(this.root, source.fileName),
1673
1898
  position: attribute.getStart(),
1674
1899
  line: source.getLineAndCharacterOfPosition(attribute.getStart()).line + 1
1675
1900
  }
@@ -1678,10 +1903,11 @@ class Evidence {
1678
1903
  }
1679
1904
  element(node) {
1680
1905
  const component = this.functionValue(node.tagName);
1681
- if (!component)
1906
+ if (!component || !this.sameBuild(node.getSourceFile(), component.getSourceFile()))
1682
1907
  return;
1683
1908
  if (node.attributes.properties.some(ts4.isJsxSpreadAttribute)) {
1684
- this.unsupported(node);
1909
+ if (this.propCalls(component)?.length !== 0)
1910
+ this.unsupported(node, "a spread may supply a prop the component calls");
1685
1911
  return;
1686
1912
  }
1687
1913
  for (const attribute of node.attributes.properties)
@@ -1694,59 +1920,230 @@ class Evidence {
1694
1920
  ts4.forEachChild(node, (child) => this.inspect(child));
1695
1921
  }
1696
1922
  }
1697
- async function checkReactReadiness(root) {
1698
- const projects = await frameworkProjects(root, "react", [".tsx"]);
1699
- for (const project of projects)
1700
- reactProject(project, root);
1923
+
1924
+ // plugins/scanners/react/src/http.ts
1925
+ import ts5 from "./typescript.cjs";
1926
+
1927
+ // plugins/scanners/http-clients.ts
1928
+ async function declaredMethod(context, options, fallback) {
1929
+ if (options === undefined)
1930
+ return fallback;
1931
+ const declared = await heldAt(context, options, "method");
1932
+ if (declared === "absent")
1933
+ return fallback;
1934
+ return typeof declared === "string" ? undefined : methodName(context, declared.node);
1935
+ }
1936
+ async function optionsBase(context, config, fallback) {
1937
+ if (config === undefined)
1938
+ return fallback;
1939
+ const base = await heldAt(context, config, "baseURL");
1940
+ return base === "absent" ? fallback : heldParts(context, base);
1701
1941
  }
1702
- async function scanReact(root) {
1703
- const parts = [];
1704
- for (const project of await frameworkProjects(root, "react", [".tsx"])) {
1705
- const observation = await scanReactProject(project, root);
1706
- if (observation)
1707
- parts.push({ key: relative(root, project), observation });
1942
+ async function runtimeFetch(context, callee) {
1943
+ if (!context.ts.isIdentifier(callee))
1944
+ return;
1945
+ const origin = await importOrigin(context, callee);
1946
+ const fetch = origin === undefined ? callee.text === "fetch" && await runtimeGlobal(context, callee) : origin.module === "node-fetch" && origin.name === "default" || origin.module === "undici" && origin.name === "fetch";
1947
+ return fetch ? "fetch" : undefined;
1948
+ }
1949
+ async function urlWithoutMethod(context, url, input) {
1950
+ if (input === "unseen")
1951
+ return true;
1952
+ if (typeof input === "object") {
1953
+ const value = unwrapped(context.ts, input.node);
1954
+ if (urlText(context.ts, value))
1955
+ return true;
1956
+ const urlObject = context.ts.isNewExpression(value) && context.ts.isIdentifier(value.expression) && value.expression.text === "URL" && await runtimeGlobal(context, value.expression);
1957
+ if (urlObject)
1958
+ return true;
1708
1959
  }
1709
- return combineObservations(parts);
1960
+ return context.checker.primitiveAt(url);
1710
1961
  }
1711
- async function scanReactProject(projectRoot, root) {
1712
- const project = reactProject(projectRoot, root);
1713
- if (!project)
1962
+ async function fetchRequest(context, call, client) {
1963
+ const [url, options] = call.arguments;
1964
+ const kind = url === undefined ? undefined : await client(call.expression);
1965
+ if (url === undefined || kind === undefined)
1714
1966
  return;
1715
- const { manifest, program, owned, sources, routes, routers } = project;
1716
- const evidence = new Evidence(root, program.getTypeChecker(), sources);
1967
+ const input = await heldAt(context, url);
1968
+ const method = await urlWithoutMethod(context, url, input) ? await declaredMethod(context, options, "GET") : undefined;
1969
+ const parts = await urlParts(context, url);
1970
+ const target = kind === "ofetch" ? withBase(await optionsBase(context, options, []), parts) : parts;
1971
+ return { ...method === undefined ? {} : { method }, ...requestUrl(target) };
1972
+ }
1973
+ async function withDefaults(context, client, holders) {
1974
+ const found = await Promise.all(holders.map((holder) => context.bindings.settings(holder, ["defaults"], ["baseURL", "method"])));
1975
+ const bases = found.flatMap((settings) => settings.assigned.get("baseURL") ?? []);
1976
+ const methods = found.flatMap((settings) => settings.assigned.get("method") ?? []);
1977
+ if (found.some((settings) => settings.changed) || bases.length > 1 || methods.length > 1)
1978
+ return { base: [computedPart], method: undefined };
1979
+ return {
1980
+ base: bases[0] === undefined ? client.base : await urlParts(context, bases[0]),
1981
+ method: methods[0] === undefined ? client.method : methodText(await literalText(context, methods[0]))
1982
+ };
1983
+ }
1984
+ function defaultClient(context) {
1985
+ return withDefaults(context, { base: [], method: "GET" }, context.bindings.defaultImports("axios"));
1986
+ }
1987
+ async function sent(context, client, url, method, config) {
1988
+ const effective = method ?? await declaredMethod(context, config, client.method);
1989
+ const base = await optionsBase(context, config, client.base);
1990
+ return { ...effective === undefined ? {} : { method: effective }, ...requestUrl(joinBase(base, url)) };
1991
+ }
1992
+ async function configRequest(context, client, config) {
1993
+ const url = config === undefined ? "absent" : await heldAt(context, config, "url");
1994
+ return url === "absent" ? undefined : sent(context, client, await heldParts(context, url), undefined, config);
1995
+ }
1996
+ async function holdsObject(context, node) {
1997
+ const value = await heldAt(context, node);
1998
+ return typeof value === "object" && context.ts.isObjectLiteralExpression(value.node);
1999
+ }
2000
+ var SHORTHAND = new Map([
2001
+ ["get", "GET"],
2002
+ ["post", "POST"],
2003
+ ["put", "PUT"],
2004
+ ["patch", "PATCH"],
2005
+ ["delete", "DELETE"],
2006
+ ["head", "HEAD"],
2007
+ ["options", "OPTIONS"],
2008
+ ["postForm", "POST"],
2009
+ ["putForm", "PUT"],
2010
+ ["patchForm", "PATCH"]
2011
+ ]);
2012
+ var WITH_BODY = new Set(["post", "put", "patch", "postForm", "putForm", "patchForm"]);
2013
+ async function isAxios(context, node) {
2014
+ const origin = await importOrigin(context, node);
2015
+ return origin?.module === "axios" && origin.name === "default";
2016
+ }
2017
+ async function createdClient(context, node) {
2018
+ const { ts } = context;
2019
+ const created = await heldAt(context, node);
2020
+ const call = typeof created === "object" && ts.isCallExpression(created.node) ? created.node : undefined;
2021
+ const callee = call?.expression;
2022
+ if (call === undefined || callee === undefined || !ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name))
2023
+ return;
2024
+ if (callee.name.text !== "create" || !await isAxios(context, callee.expression))
2025
+ return;
2026
+ const parent = await defaultClient(context);
2027
+ const inherited = { base: parent.base.length === 0 ? [] : [computedPart], method: parent.method === "GET" ? "GET" : undefined };
2028
+ const [config] = call.arguments;
2029
+ const client = { base: await optionsBase(context, config, inherited.base), method: await declaredMethod(context, config, inherited.method) };
2030
+ const holders = [await declarationOf(context, node), holderOf(ts, call)].filter((holder) => holder !== undefined);
2031
+ return withDefaults(context, client, [...new Set(holders)]);
2032
+ }
2033
+ async function axiosClient(context, node) {
2034
+ if (!context.ts.isIdentifier(node))
2035
+ return;
2036
+ return await isAxios(context, node) ? defaultClient(context) : createdClient(context, node);
2037
+ }
2038
+ async function memberRequest(context, receiver, member, call) {
2039
+ const method = SHORTHAND.get(member);
2040
+ if (method === undefined && member !== "request")
2041
+ return;
2042
+ const client = await axiosClient(context, receiver);
2043
+ if (client === undefined)
2044
+ return;
2045
+ const [first, second, third] = call.arguments;
2046
+ if (method !== undefined && first !== undefined) {
2047
+ return sent(context, client, await urlParts(context, first), method, WITH_BODY.has(member) ? third : second);
2048
+ }
2049
+ return member === "request" ? configRequest(context, client, first) : undefined;
2050
+ }
2051
+ async function axiosRequest(context, call) {
2052
+ const { ts } = context;
2053
+ const callee = call.expression;
2054
+ const [first, second] = call.arguments;
2055
+ if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
2056
+ return memberRequest(context, callee.expression, callee.name.text, call);
2057
+ }
2058
+ const client = await axiosClient(context, callee);
2059
+ if (client === undefined || first === undefined)
2060
+ return;
2061
+ if (await holdsObject(context, first))
2062
+ return configRequest(context, client, first);
2063
+ return sent(context, client, await urlParts(context, first), undefined, second);
2064
+ }
2065
+
2066
+ // plugins/scanners/react/src/http.ts
2067
+ async function reactHttpRequests(sources, analyzed, checker, callerOperation) {
2068
+ const context = urlContext(ts5, classicChecker(ts5, checker), analyzed);
2069
+ const calls = [];
2070
+ const visit = (node) => {
2071
+ if (ts5.isCallExpression(node))
2072
+ calls.push(node);
2073
+ ts5.forEachChild(node, visit);
2074
+ };
1717
2075
  for (const source of sources)
1718
- evidence.inspect(source);
1719
- const httpRequests = await reactHttpRequests(sources, owned, program.getTypeChecker(), (call) => {
1720
- const operation = caller(call);
1721
- return operation === undefined ? undefined : evidence.operationId(operation);
2076
+ visit(source);
2077
+ const requests = [];
2078
+ for (const call of calls) {
2079
+ const request = await fetchRequest(context, call, (callee) => runtimeFetch(context, callee)) ?? await axiosRequest(context, call);
2080
+ if (request !== undefined)
2081
+ requests.push({ operation: callerOperation(call), ...request });
2082
+ }
2083
+ return requests;
2084
+ }
2085
+
2086
+ // plugins/scanners/react/src/scan.ts
2087
+ function reactProjects(root, files) {
2088
+ return frameworkProjects(root, files, "react", [".tsx"], { inheritConfig: true });
2089
+ }
2090
+ async function eachProject(root, files, visit) {
2091
+ const typescriptSources = new Set(typeScriptSources(files));
2092
+ const configs = new Set(files.filter((file) => path8.posix.basename(file) === "tsconfig.json"));
2093
+ const directories = await reactProjects(root, files);
2094
+ const packages = directories.map((directory) => relative2(root, directory)).sort((left, right) => right.length - left.length);
2095
+ for (const directory of directories) {
2096
+ const project = reactProject(directory, root, typescriptSources, configs, packages);
2097
+ if (project !== undefined)
2098
+ await visit(directory, project);
2099
+ }
2100
+ }
2101
+ async function checkReactReadiness(root, _settings, files) {
2102
+ await eachProject(root, files, async () => {});
2103
+ }
2104
+ async function scanReact(root, _settings, files) {
2105
+ const parts = [];
2106
+ await eachProject(root, files, async (directory, project) => {
2107
+ const observation = await scanProject(root, directory, project, files);
2108
+ if (observation)
2109
+ parts.push({ key: relative2(root, directory), observation });
1722
2110
  });
1723
- const httpEndpoints = nextRouteEndpoints(routes, projectRoot, routers, (handler) => evidence.operationId(handler));
1724
- const files = [...new Set([...sources, ...routes].map((source) => relative(root, source.fileName)))].map((file) => ({ file, symbols: [] }));
1725
- return createScanObservation({
1726
- scanner: { id: "react", technology: "typescript/react", engine: "typescript-sdk", engineVersion: ts4.version },
1727
- roots: [{ id: "react-project", kind: "package", name: manifest.name, file: relative(root, path5.join(projectRoot, "package.json")) }],
1728
- files: files.map((file) => ({ ...file, roots: ["react-project"] })),
2111
+ return combineObservations(parts);
2112
+ }
2113
+ async function scanProject(root, directory, { manifest, program, routers, readable, owned, components, routes, diagnostics, owner }, files) {
2114
+ const evidence = new Evidence(root, program, readable);
2115
+ for (const source of components)
2116
+ evidence.inspect(source);
2117
+ const httpRequests = await reactHttpRequests(components, readable, program.getTypeChecker(), (call) => evidence.operationId(caller(call) ?? call.getSourceFile()));
2118
+ const httpEndpoints = nextRouteEndpoints(routes, directory, routers, (handler) => evidence.operationId(handler));
2119
+ const reported = [...new Set([
2120
+ ...[...components, ...routes].map((source) => relative2(root, source.fileName)),
2121
+ ...[...evidence.operations.values()].map((operation) => operation.file)
2122
+ ])];
2123
+ const own = relative2(root, directory);
2124
+ const packageRoot = (file) => owner(file) === own ? "react-project" : `package:${owner(file)}`;
2125
+ const others = [...new Set(reported.map(owner))].filter((item) => item !== own);
2126
+ return withJavaScriptEntries(root, createScanObservation({
2127
+ scanner: { id: "react", technology: "typescript/react", engine: "typescript-sdk", engineVersion: ts6.version },
2128
+ roots: [
2129
+ { id: "react-project", kind: "package", name: manifest.name, file: relative2(root, path8.join(directory, "package.json")) },
2130
+ ...others.map((item) => ({ id: `package:${item}`, kind: "package", name: item || ".", file: path8.posix.join(item, "package.json") }))
2131
+ ],
2132
+ files: reported.map((file) => ({ file, symbols: [], roots: [packageRoot(file)] })),
1729
2133
  operations: [...evidence.operations.values()],
1730
2134
  invocations: evidence.invocations,
1731
- diagnostics: evidence.diagnostics,
2135
+ diagnostics: [...diagnostics, ...evidence.diagnostics],
1732
2136
  ...httpEndpoints.length ? { httpEndpoints } : {},
1733
2137
  ...httpRequests.length ? { httpRequests } : {}
1734
- });
2138
+ }), await entrySourceInputs(root, ts6, classicChecker(ts6, program.getTypeChecker()), owned), files);
1735
2139
  }
1736
2140
 
1737
2141
  // plugins/scanners/react/src/index.ts
1738
2142
  var src_default = {
1739
2143
  id: "react",
1740
- watch: { include: ["**/*.tsx", "**/*.ts", "**/tsconfig*.json", "**/package.json"], exclude: [] },
1741
2144
  checkReadiness: checkReactReadiness,
1742
- listSourceFiles: (root) => frameworkSourceFiles({
1743
- root,
1744
- dependency: "react",
1745
- projects: [".tsx"],
1746
- sources: [".tsx"],
1747
- also: routeCandidate
1748
- }),
1749
- readCodeStructure: (root, references) => readTypeScriptOutline(ts5, root, references),
2145
+ listSourceFiles: async (root, _settings, candidates) => (await reactProjects(root, candidates)).length ? typeScriptSources(candidates) : [],
2146
+ readCodeStructure: (root, references) => readTypeScriptOutline(ts7, root, references),
1750
2147
  scan: scanReact
1751
2148
  };
1752
2149
  export {