@groma/scanner-javascript 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // plugins/scanners/javascript/src/index.ts
3
- import path4 from "path";
3
+ import path7 from "path";
4
4
 
5
5
  // packages/scanner/src/values.ts
6
6
  function object(value, label) {
@@ -118,13 +118,13 @@ function compare(...values) {
118
118
  }
119
119
  function uniqueBy(values, key, label) {
120
120
  const seen = new Set;
121
- for (const value of values) {
122
- const id = key(value);
121
+ const keyed = values.map((value) => ({ id: key(value), value }));
122
+ for (const { id } of keyed) {
123
123
  if (seen.has(id))
124
124
  throw new Error(`duplicate ${label}: ${id}`);
125
125
  seen.add(id);
126
126
  }
127
- return [...values].sort((left, right) => key(left).localeCompare(key(right)));
127
+ return keyed.sort((left, right) => left.id.localeCompare(right.id)).map((entry) => entry.value);
128
128
  }
129
129
  function createScanObservation(input) {
130
130
  const roots = validateRoots(input.roots);
@@ -135,9 +135,7 @@ function createScanObservation(input) {
135
135
  symbols: [...new Map(file.symbols.map((symbol) => [
136
136
  compare(symbol.id, symbol.kind),
137
137
  symbol
138
- ])).values()].sort((left, right) => {
139
- return compare(left.id, left.kind).localeCompare(compare(right.id, right.kind));
140
- })
138
+ ]))].sort(([left], [right]) => left.localeCompare(right)).map(([, symbol]) => symbol)
141
139
  })), (file) => file.file, "file path");
142
140
  const filePaths = new Set(files.map((file) => file.file));
143
141
  for (const file of files) {
@@ -148,24 +146,46 @@ function createScanObservation(input) {
148
146
  throw new Error(`file references unknown root: ${root}`);
149
147
  }
150
148
  }
151
- for (const diagnostic of input.diagnostics)
152
- diagnosticLocation(diagnostic);
153
149
  return {
154
150
  schemaVersion: 1,
155
151
  scanner: input.scanner,
156
152
  roots,
157
153
  files,
158
154
  ...sourceUnits(input.sourceUnits, filePaths),
155
+ ...entryPoints(input.entryPoints, filePaths),
159
156
  ...operationEvidence(input, filePaths),
160
157
  ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
161
- diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
162
- diagnosticKey(diagnostic),
163
- diagnostic
164
- ])).values()].sort((left, right) => {
165
- return diagnosticKey(left).localeCompare(diagnosticKey(right));
166
- })
158
+ diagnostics: normalizeScanDiagnostics(input.diagnostics)
167
159
  };
168
160
  }
161
+ function normalizeScanDiagnostics(diagnostics) {
162
+ for (const diagnostic of diagnostics)
163
+ diagnosticLocation(diagnostic);
164
+ return [...new Map(diagnostics.map((diagnostic) => [diagnosticKey(diagnostic), diagnostic]))].sort(([left], [right]) => left.localeCompare(right)).map(([, diagnostic]) => diagnostic);
165
+ }
166
+ function entryPoints(input, paths) {
167
+ if (input === undefined)
168
+ return {};
169
+ const entries = array(input, "entryPoints").map((value) => {
170
+ const entry = object(value, "entry point");
171
+ const files = [...new Set(array(entry.files, "entry point.files").map((file) => string(file, "entry point.file")))].sort();
172
+ for (const file of files) {
173
+ if (!paths.has(file))
174
+ throw new Error(`entry point references unknown file: ${file}`);
175
+ }
176
+ const file = string(entry.file, "entry point.file");
177
+ if (!files.includes(file))
178
+ throw new Error(`entry point must include its own source file: ${file}`);
179
+ return {
180
+ file,
181
+ declaration: string(entry.declaration, "entry point.declaration"),
182
+ name: string(entry.name, "entry point.name"),
183
+ files
184
+ };
185
+ });
186
+ const key = (entry) => compare(entry.file, entry.declaration, entry.name, ...entry.files);
187
+ return { entryPoints: [...new Map(entries.map((entry) => [key(entry), entry])).values()].sort((a, b) => key(a).localeCompare(key(b))) };
188
+ }
169
189
  function sourceUnits(input, paths) {
170
190
  if (input === undefined)
171
191
  return {};
@@ -251,10 +271,14 @@ function operationTokens(operation) {
251
271
  const endLine = Number(operation.endLine);
252
272
  if (endLine < startLine)
253
273
  throw new Error("operation.endLine must be at or after startLine");
274
+ const tokens = array(operation.tokens, "operation.tokens");
275
+ tokens.forEach((token) => {
276
+ string(token, "operation.token");
277
+ });
254
278
  return {
255
279
  startLine,
256
280
  endLine,
257
- tokens: array(operation.tokens, "operation.tokens").map((token) => string(token, "operation.token"))
281
+ tokens
258
282
  };
259
283
  }
260
284
  function validateOperationTokens(operation) {
@@ -284,7 +308,7 @@ function sourcePosition(position) {
284
308
  }
285
309
 
286
310
  // plugins/scanners/javascript/src/index.ts
287
- import ts6 from "./typescript.cjs";
311
+ import ts7 from "./typescript.cjs";
288
312
 
289
313
  // plugins/scanners/javascript/src/evidence.ts
290
314
  import ts2 from "./typescript.cjs";
@@ -577,12 +601,40 @@ import ts from "./typescript.cjs";
577
601
  function isFunctionValue(node) {
578
602
  return node !== undefined && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
579
603
  }
604
+ function isExportsReference(node) {
605
+ if (ts.isIdentifier(node))
606
+ return node.text === "exports";
607
+ return ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
608
+ }
609
+ function commonJsExport(statement) {
610
+ if (!ts.isExpressionStatement(statement))
611
+ return;
612
+ let expression = statement.expression;
613
+ while (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
614
+ const { left } = expression;
615
+ if (ts.isPropertyAccessExpression(left) && (isExportsReference(left) || isExportsReference(left.expression)))
616
+ return expression;
617
+ expression = expression.right;
618
+ }
619
+ return;
620
+ }
621
+ function assignedValue(expression) {
622
+ return ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken ? assignedValue(expression.right) : expression;
623
+ }
624
+ function exportedClass(statement) {
625
+ const assignment = commonJsExport(statement);
626
+ const value = assignment && assignedValue(assignment.right);
627
+ return value && ts.isClassExpression(value) && value.name ? [{ name: value.name.text, kind: "class" }] : [];
628
+ }
580
629
  function topLevelDeclarations(source) {
581
630
  return source.statements.flatMap((statement) => {
582
631
  if (ts.isFunctionDeclaration(statement))
583
632
  return statement.name ? [{ name: statement.name.text, kind: "function" }] : [];
584
633
  if (ts.isClassDeclaration(statement))
585
634
  return statement.name ? [{ name: statement.name.text, kind: "class" }] : [];
635
+ const published = exportedClass(statement);
636
+ if (published.length)
637
+ return published;
586
638
  if (!ts.isVariableStatement(statement))
587
639
  return [];
588
640
  return statement.declarationList.declarations.flatMap((declaration) => ts.isIdentifier(declaration.name) && isFunctionValue(declaration.initializer) ? [{ name: declaration.name.text, kind: "function" }] : []);
@@ -624,7 +676,7 @@ function javaScriptEvidence(file, source) {
624
676
  });
625
677
  }
626
678
  function visit(node, owner) {
627
- const inside = executable(node) ? operation(node) : owner;
679
+ const inside = executable(node) || ts2.isGetAccessorDeclaration(node) || ts2.isSetAccessorDeclaration(node) ? operation(node) : owner;
628
680
  if (ts2.isCallExpression(node)) {
629
681
  calls.push(node);
630
682
  invocation(node, inside ?? moduleOperation());
@@ -648,6 +700,12 @@ function javaScriptEvidence(file, source) {
648
700
  import ts4 from "./typescript.cjs";
649
701
 
650
702
  // plugins/scanners/http-checker.ts
703
+ async function inBatches(items, ask) {
704
+ const answers = [];
705
+ for (let offset = 0;offset < items.length; offset += 256)
706
+ answers.push(...await Promise.all(items.slice(offset, offset + 256).map(ask)));
707
+ return answers;
708
+ }
651
709
  function primitive(ts, type) {
652
710
  if (type.isUnion())
653
711
  return (type.types ?? []).every((member) => primitive(ts, member));
@@ -806,6 +864,8 @@ var PLACEHOLDER = /^:([A-Za-z_]\w*)(?:\((.*)\)|\{(.*)\})?(\?)?$/;
806
864
  var FIRST_PLACEHOLDER = /:([A-Za-z_]\w*)/;
807
865
  var WILDCARD = /^\*([A-Za-z_]\w*)?$/;
808
866
  var OPTIONAL_GROUP = /\{\/:([A-Za-z_]\w*)\}$/;
867
+ var OPTIONAL_WILDCARD = /(?:\{\/\*|\/\{\*)([A-Za-z_]\w*)\}$/;
868
+ var SEGMENT_GROUP = "{/";
809
869
  var SYNTAX = /[:*(){}?+]/;
810
870
  var ONE_SEGMENT = /^(?:\^|\$|\\[dw]|[A-Za-z0-9_-]|\[(?:\\[dw]|[A-Za-z0-9_]-[A-Za-z0-9_]|[A-Za-z0-9_-])+\]|[+*?|]|\{\d+(?:,\d*)?\}|\(\?:|[()])+$/;
811
871
  var REMAINDER = new Set([".*", ".+"]);
@@ -840,10 +900,16 @@ function segment(part, last, bareWildcard) {
840
900
  return { kind: "parameter", name: first[1], constrained: true };
841
901
  }
842
902
  function endpointPath(pattern, bareWildcard = false) {
843
- const group = OPTIONAL_GROUP.exec(pattern);
903
+ const group = OPTIONAL_GROUP.exec(pattern) ?? OPTIONAL_WILDCARD.exec(pattern);
844
904
  if (group !== null) {
845
905
  const path = endpointPath(pattern.slice(0, group.index), bareWildcard);
846
- return path.at(-1)?.kind === "catch-all" ? path : [...path, { kind: "parameter", name: group[1], optional: true }];
906
+ const optional = group[0].includes("*") ? { kind: "catch-all", name: group[1], optional: true } : { kind: "parameter", name: group[1], optional: true };
907
+ return path.at(-1)?.kind === "catch-all" ? path : [...path, optional];
908
+ }
909
+ const opening = pattern.indexOf(SEGMENT_GROUP);
910
+ if (opening >= 0) {
911
+ const path = endpointPath(pattern.slice(0, opening), bareWildcard);
912
+ return path.at(-1)?.kind === "catch-all" ? path : [...path, remainder()];
847
913
  }
848
914
  const pieces = pattern.split("/").filter((part) => part !== "");
849
915
  const path = [];
@@ -888,8 +954,10 @@ function ownProperty(ts, object, name) {
888
954
  let found = "absent";
889
955
  for (const property of object.properties) {
890
956
  const key = propertyKey(ts, property);
891
- if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property))
892
- return "unknown";
957
+ if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) {
958
+ found = "unknown";
959
+ continue;
960
+ }
893
961
  if (key === name)
894
962
  found = ts.isPropertyAssignment(property) ? { node: property.initializer } : "unknown";
895
963
  }
@@ -1338,6 +1406,10 @@ async function variableHeld(context, name, path, depth) {
1338
1406
  return "unseen";
1339
1407
  if (path.length === 0 && ts.isFunctionDeclaration(declaration))
1340
1408
  return { node: declaration };
1409
+ if (path.length === 1 && ts.isEnumDeclaration(declaration)) {
1410
+ const member = declaration.members.find((entry) => ts.isIdentifier(entry.name) && entry.name.text === path[0]);
1411
+ return member?.initializer === undefined ? "unknown" : held(context, member.initializer, [], depth + 1);
1412
+ }
1341
1413
  if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined)
1342
1414
  return "unknown";
1343
1415
  if (!ts.isIdentifier(declaration.name) || !await unassigned(context, declaration))
@@ -1547,12 +1619,14 @@ async function collectRegistrars(context, sources) {
1547
1619
  }
1548
1620
  });
1549
1621
  }
1550
- const registrars = new Map;
1551
- for (const declaration of declarations) {
1622
+ const found = await inBatches(declarations, async (declaration) => {
1552
1623
  const registrar = await registrarOf(context, declaration.initializer, declaration.name.text);
1553
- if (registrar !== undefined && await unassigned(context, declaration))
1554
- registrars.set(declaration, registrar);
1555
- }
1624
+ return registrar !== undefined && await unassigned(context, declaration) ? registrar : undefined;
1625
+ });
1626
+ const registrars = new Map;
1627
+ for (const [index, registrar] of found.entries())
1628
+ if (registrar !== undefined)
1629
+ registrars.set(declarations[index], registrar);
1556
1630
  return registrars;
1557
1631
  }
1558
1632
  function candidate(member) {
@@ -1570,11 +1644,15 @@ function registers(member, framework) {
1570
1644
  return framework === "fastify";
1571
1645
  if (member === "route")
1572
1646
  return framework !== "koa";
1573
- return routeMembers.has(member) || member === "use";
1647
+ if (member === "use")
1648
+ return framework !== "hono";
1649
+ return routeMembers.has(member);
1574
1650
  }
1575
1651
  function returnsRegistrar(member, framework) {
1576
1652
  if (framework === "express" && expressSettings.has(member))
1577
1653
  return true;
1654
+ if (framework === "hono" && member === "use")
1655
+ return true;
1578
1656
  if (framework === "koa" && member === "prefix")
1579
1657
  return true;
1580
1658
  const builder = member === "route" && framework === "express" || member === "basePath";
@@ -1596,17 +1674,19 @@ async function receiverDeclaration(context, call, registrars) {
1596
1674
  return declaration !== undefined && registrars.has(declaration) ? declaration : undefined;
1597
1675
  }
1598
1676
  async function collectRegistrations(context, calls, registrars) {
1599
- const registrations = [];
1600
- for (const call of calls) {
1677
+ const candidates = calls.flatMap((call) => {
1601
1678
  const member = memberOf(context.ts, call);
1602
- if (member === undefined || !candidate(member))
1603
- continue;
1604
- const declaration = await receiverDeclaration(context, call, registrars);
1605
- if (declaration !== undefined && registers(member, registrars.get(declaration).framework)) {
1606
- registrations.push({ call, declaration, member });
1607
- }
1608
- }
1609
- return registrations;
1679
+ return member !== undefined && candidate(member) ? [{ call, member }] : [];
1680
+ });
1681
+ const declarations = await inBatches(candidates, ({ call }) => receiverDeclaration(context, call, registrars));
1682
+ return candidates.flatMap(({ call, member }, index) => {
1683
+ const declaration = declarations[index];
1684
+ if (declaration === undefined)
1685
+ return [];
1686
+ const framework = registrars.get(declaration).framework;
1687
+ const incomplete = routeMethod(member, framework) !== undefined && call.arguments.length < 2;
1688
+ return registers(member, framework) && !incomplete ? [{ call, declaration, member }] : [];
1689
+ });
1610
1690
  }
1611
1691
  async function applyPrefixes(context, calls, registrars) {
1612
1692
  for (const call of calls) {
@@ -1642,7 +1722,7 @@ function exportOf(ts, reference) {
1642
1722
  const parent = reference.parent;
1643
1723
  if (ts.isExportAssignment(parent) || ts.isExportSpecifier(parent))
1644
1724
  return "last";
1645
- const assigned = ts.isBinaryExpression(parent) && parent.right === reference && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && commonJsExport(ts, parent.left);
1725
+ const assigned = ts.isBinaryExpression(parent) && parent.right === reference && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && commonJsExport2(ts, parent.left);
1646
1726
  if (!assigned)
1647
1727
  return;
1648
1728
  return atTopLevel(ts, parent) ? "last" : "anywhere";
@@ -1653,12 +1733,12 @@ function exportedDeclaration(ts, declaration) {
1653
1733
  return declaration.parent.parent.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
1654
1734
  }
1655
1735
  var httpModules = new Set(["http", "https", "node:http", "node:https"]);
1656
- function commonJsExport(ts, node) {
1736
+ function commonJsExport2(ts, node) {
1657
1737
  if (!ts.isPropertyAccessExpression(node))
1658
1738
  return false;
1659
1739
  const root = node.expression;
1660
1740
  if (!ts.isIdentifier(root))
1661
- return commonJsExport(ts, root);
1741
+ return commonJsExport2(ts, root);
1662
1742
  return root.text === "exports" || root.text === "module" && node.name.text === "exports";
1663
1743
  }
1664
1744
  async function serving(context, reference) {
@@ -1708,39 +1788,12 @@ async function collectHandOffs(context, registrars, registrations) {
1708
1788
  }
1709
1789
  return handOffs;
1710
1790
  }
1711
- function entryStart(ts, node) {
1712
- return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) ? node.expression.name.getStart() : node.getStart();
1713
- }
1714
- function orderIndices(ts, registrations, handOffs) {
1715
- const entries = [
1716
- ...registrations.map(({ call, declaration }) => ({ node: call, declaration, start: entryStart(ts, call), last: false })),
1717
- ...handOffs.map(({ reference, declaration, last }) => ({
1718
- node: reference,
1719
- declaration,
1720
- start: last ? Number.POSITIVE_INFINITY : reference.getStart(),
1721
- last
1722
- }))
1723
- ];
1724
- const byRegistrar = new Map;
1725
- for (const entry of entries)
1726
- byRegistrar.set(entry.declaration, [...byRegistrar.get(entry.declaration) ?? [], entry]);
1727
- const indices = new Map;
1728
- for (const list of byRegistrar.values()) {
1729
- const files = new Set(list.map(({ node }) => node.getSourceFile()));
1730
- if (files.size !== 1 || !list.every(({ node, last }) => last || atTopLevel(ts, node)))
1731
- continue;
1732
- const sorted = [...list].sort((left, right) => left.start - right.start);
1733
- for (const [index, { node }] of sorted.entries())
1734
- indices.set(node, index);
1735
- }
1736
- return indices;
1737
- }
1738
1791
  async function routerRegistrations(context, sources, calls) {
1739
1792
  const registrars = await collectRegistrars(context, sources);
1740
1793
  await applyPrefixes(context, calls, registrars);
1741
1794
  const registrations = await collectRegistrations(context, calls, registrars);
1742
1795
  const handOffs = await collectHandOffs(context, registrars, registrations);
1743
- return { registrars, registrations, handOffs, indices: orderIndices(context.ts, registrations, handOffs) };
1796
+ return { registrars, registrations, handOffs };
1744
1797
  }
1745
1798
 
1746
1799
  // plugins/scanners/http-routes.ts
@@ -1763,25 +1816,35 @@ async function foreignRouter(reading, argument) {
1763
1816
  const origin = await importOrigin(context, argument);
1764
1817
  if (origin?.module.startsWith(".") !== true)
1765
1818
  return false;
1819
+ return !await seenFunction(context, argument);
1820
+ }
1821
+ async function seenFunction(context, argument) {
1766
1822
  const held = await heldAt(context, argument);
1767
- return typeof held !== "object" || !ts.isFunctionLike(held.node);
1823
+ return typeof held === "object" && context.ts.isFunctionLike(held.node);
1768
1824
  }
1769
1825
  async function middleware(context, argument) {
1770
1826
  const { ts } = context;
1771
- const held = await heldAt(context, argument);
1772
- if (typeof held === "object" && ts.isFunctionLike(held.node))
1827
+ if (await seenFunction(context, argument))
1773
1828
  return true;
1774
1829
  const callee = ts.isCallExpression(argument) ? argument.expression : argument;
1775
1830
  const holder = ts.isPropertyAccessExpression(callee) ? callee.expression : callee;
1776
1831
  const origin = await importOrigin(context, holder);
1777
1832
  return origin !== undefined && !origin.module.startsWith(".");
1778
1833
  }
1834
+ async function onlyMiddleware(reading, registration, handlers) {
1835
+ if (registration.member !== "use")
1836
+ return false;
1837
+ return (await Promise.all(handlers.map((handler) => seenFunction(reading.context, handler)))).every(Boolean);
1838
+ }
1779
1839
  async function readMount(reading, registration) {
1780
1840
  const [first, ...rest] = registration.call.arguments;
1841
+ const nothing = { children: [], prefix: "", blocks: false };
1781
1842
  if (first === undefined)
1782
- return { children: [], prefix: "", pathless: true, foreign: false };
1843
+ return nothing;
1783
1844
  const leading = await registrarArgument(reading, first);
1784
1845
  const pathless = leading !== undefined || await foreignRouter(reading, first) || await middleware(reading.context, first);
1846
+ if (!pathless && await onlyMiddleware(reading, registration, rest))
1847
+ return nothing;
1785
1848
  const children = leading === undefined ? [] : [leading];
1786
1849
  for (const argument of rest) {
1787
1850
  const child = await registrarArgument(reading, argument);
@@ -1790,7 +1853,9 @@ async function readMount(reading, registration) {
1790
1853
  }
1791
1854
  const prefix = pathless ? "" : await literalText(reading.context, first);
1792
1855
  const foreign = children.length === 0 && (await Promise.all(registration.call.arguments.map((argument) => foreignRouter(reading, argument)))).some(Boolean);
1793
- return { children, pathless, foreign, ...prefix === undefined ? {} : { prefix } };
1856
+ const hidden = children.some((child) => reading.registrars.get(child).prefix === undefined);
1857
+ const unfollowed = prefix === undefined ? children.length > 0 : !pathless && children.length === 0;
1858
+ return { children, blocks: foreign || hidden || unfollowed, ...prefix === undefined ? {} : { prefix } };
1794
1859
  }
1795
1860
  async function collectMounts(reading) {
1796
1861
  const mounts = new Map;
@@ -1807,20 +1872,59 @@ async function collectMounts(reading) {
1807
1872
  }
1808
1873
  return { mounts, calls };
1809
1874
  }
1875
+ function entryStart(ts, node) {
1876
+ return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) ? node.expression.name.getStart() : node.getStart();
1877
+ }
1878
+ function orderIndices(reading, calls) {
1879
+ const { ts } = reading.context;
1880
+ const places = (registration) => {
1881
+ const mount = calls.get(registration);
1882
+ return mount === undefined || mount.children.length > 0 || mount.blocks;
1883
+ };
1884
+ const entries = [
1885
+ ...reading.registrations.filter(places).map(({ call, declaration }) => ({ node: call, declaration, start: entryStart(ts, call), last: false })),
1886
+ ...reading.handOffs.map(({ reference, declaration, last }) => ({
1887
+ node: reference,
1888
+ declaration,
1889
+ start: last ? Number.POSITIVE_INFINITY : reference.getStart(),
1890
+ last
1891
+ }))
1892
+ ];
1893
+ const byRegistrar = new Map;
1894
+ for (const entry of entries)
1895
+ byRegistrar.set(entry.declaration, [...byRegistrar.get(entry.declaration) ?? [], entry]);
1896
+ const indices = new Map;
1897
+ for (const list of byRegistrar.values()) {
1898
+ const files = new Set(list.map(({ node }) => node.getSourceFile()));
1899
+ if (files.size !== 1 || !list.every(({ node, last }) => last || atTopLevel(ts, node)))
1900
+ continue;
1901
+ const sorted = [...list].sort((left, right) => left.start - right.start);
1902
+ for (const [index, { node }] of sorted.entries())
1903
+ indices.set(node, index);
1904
+ }
1905
+ return indices;
1906
+ }
1810
1907
  function withPrefix(placement, registrar) {
1811
1908
  return registrar.prefix === "" ? placement : { ...placement, prefix: `${placement.prefix}/${registrar.prefix}` };
1812
1909
  }
1813
- function underMount(routing, mount, entry) {
1910
+ function underMount(routing, mount, entry, child) {
1814
1911
  const { ts } = routing.context;
1815
1912
  const parent = routing.registrars.get(mount.declaration);
1816
1913
  const copies = parent.framework === "hono" && mount.member === "route" || parent.framework === "koa" && parent.kind === "router" && mount.member === "use";
1817
1914
  if (!copies)
1818
1915
  return "yes";
1819
- const ordered = mount.call.getSourceFile() === entry.getSourceFile() && atTopLevel(ts, mount.call) && atTopLevel(ts, entry);
1820
- if (!ordered)
1916
+ if (mount.call.getSourceFile() !== entry.getSourceFile()) {
1917
+ return entry.getSourceFile() === child.getSourceFile() && atTopLevel(ts, entry) ? "yes" : "unknown";
1918
+ }
1919
+ if (!atTopLevel(ts, mount.call) || !atTopLevel(ts, entry))
1821
1920
  return "unknown";
1822
1921
  return entryStart(ts, entry) < entryStart(ts, mount.call) ? "yes" : "no";
1823
1922
  }
1923
+ function applicationId(routing, declaration, variable) {
1924
+ const file = routing.context.file(declaration);
1925
+ const namesakes = [...routing.registrars].filter(([node, registrar]) => registrar.kind === "app" && registrar.variable === variable && routing.context.file(node) === file);
1926
+ return `${file}#${variable}${namesakes.length > 1 ? `@${declaration.getStart()}` : ""}`;
1927
+ }
1824
1928
  function placements(routing, declaration, entry, seen = new Set) {
1825
1929
  const registrar = routing.registrars.get(declaration);
1826
1930
  if (seen.has(declaration) || registrar.prefix === undefined)
@@ -1829,12 +1933,12 @@ function placements(routing, declaration, entry, seen = new Set) {
1829
1933
  if (entries.length === 0) {
1830
1934
  if (registrar.kind === "router")
1831
1935
  return [];
1832
- const application = `${routing.context.file(declaration)}#${registrar.variable}`;
1936
+ const application = applicationId(routing, declaration, registrar.variable);
1833
1937
  const ordered = orderedFrameworks.has(registrar.framework);
1834
1938
  return [withPrefix({ prefix: "", application, rank: [], known: true, ordered, certain: true }, registrar)];
1835
1939
  }
1836
1940
  return entries.flatMap(({ registration, prefix, position }) => {
1837
- const under = prefix === undefined ? "no" : underMount(routing, registration, entry);
1941
+ const under = prefix === undefined ? "no" : underMount(routing, registration, entry, declaration);
1838
1942
  if (under === "no")
1839
1943
  return [];
1840
1944
  return placements(routing, registration.declaration, registration.call, new Set([...seen, declaration])).map((parent) => {
@@ -1916,7 +2020,7 @@ async function routeCall(routing, registration, placement) {
1916
2020
  if (member === "route" && call.arguments.length === 1)
1917
2021
  return routeOptions(routing, registration, placement);
1918
2022
  const method = routeMethod(member, framework);
1919
- if (method === undefined || call.arguments.length < 2)
2023
+ if (method === undefined)
1920
2024
  return [];
1921
2025
  const parts = await urlParts(routing.context, await pathArgument(routing, registration));
1922
2026
  const route = parts.length === 1 && parts[0].kind === "text" ? parts[0].text : undefined;
@@ -1929,9 +2033,7 @@ async function routeCall(routing, registration, placement) {
1929
2033
  async function mountCall(routing, registration, placement) {
1930
2034
  const { call } = registration;
1931
2035
  const mount = routing.calls.get(registration);
1932
- const hidden = mount.children.some((child) => routing.registrars.get(child).prefix === undefined);
1933
- const unfollowed = mount.prefix === undefined ? mount.children.length > 0 : !mount.pathless && mount.children.length === 0 && call.arguments.length > 1;
1934
- if (!hidden && !mount.foreign && !unfollowed)
2036
+ if (!mount.blocks)
1935
2037
  return [];
1936
2038
  const parts = await urlParts(routing.context, call.arguments[0]);
1937
2039
  return blocked(routing, placement, mount.prefix ?? readablePrefix(parts), "*", call);
@@ -1945,7 +2047,8 @@ async function registrationEndpoints(routing, registration) {
1945
2047
  }
1946
2048
  async function routerEndpoints(context, sources, calls) {
1947
2049
  const reading = { ...await routerRegistrations(context, sources, calls), context };
1948
- const routing = { ...reading, ...await collectMounts(reading) };
2050
+ const mounted = await collectMounts(reading);
2051
+ const routing = { ...reading, ...mounted, indices: orderIndices(reading, mounted.calls) };
1949
2052
  const placed = routing.handOffs.flatMap(({ reference, declaration }) => placements(routing, declaration, reference).flatMap((placement) => blocked(routing, placement, "", "*", reference)));
1950
2053
  for (const registration of routing.registrations)
1951
2054
  placed.push(...await registrationEndpoints(routing, registration));
@@ -1969,9 +2072,7 @@ async function serveRoute(context, route, value, call) {
1969
2072
  const path = endpointPath(route);
1970
2073
  const byMethod = await objectEntries(context, value);
1971
2074
  if (byMethod === undefined) {
1972
- const held = await heldAt(context, value);
1973
- const handler = typeof held === "object" && context.ts.isFunctionLike(held.node);
1974
- return handler ? [{ operation: await context.handlerOperation(value, call), method: "*", path }] : [];
2075
+ return await seenFunction(context, value) ? [{ operation: await context.handlerOperation(value, call), method: "*", path }] : [];
1975
2076
  }
1976
2077
  const endpoints = [];
1977
2078
  for (const [key, handler] of byMethod) {
@@ -2014,19 +2115,32 @@ async function optionsBase(context, config, fallback) {
2014
2115
  return base === "absent" ? fallback : heldParts(context, base);
2015
2116
  }
2016
2117
  async function runtimeFetch(context, callee) {
2017
- if (!context.ts.isIdentifier(callee) || callee.text !== "fetch")
2118
+ if (!context.ts.isIdentifier(callee))
2018
2119
  return;
2019
2120
  const origin = await importOrigin(context, callee);
2020
- const fetch = origin === undefined ? await runtimeGlobal(context, callee) : origin.module === "node-fetch" && origin.name === "default";
2121
+ const fetch = origin === undefined ? callee.text === "fetch" && await runtimeGlobal(context, callee) : origin.module === "node-fetch" && origin.name === "default" || origin.module === "undici" && origin.name === "fetch";
2021
2122
  return fetch ? "fetch" : undefined;
2022
2123
  }
2124
+ async function urlWithoutMethod(context, url, input) {
2125
+ if (input === "unseen")
2126
+ return true;
2127
+ if (typeof input === "object") {
2128
+ const value = unwrapped(context.ts, input.node);
2129
+ if (urlText(context.ts, value))
2130
+ return true;
2131
+ const urlObject = context.ts.isNewExpression(value) && context.ts.isIdentifier(value.expression) && value.expression.text === "URL" && await runtimeGlobal(context, value.expression);
2132
+ if (urlObject)
2133
+ return true;
2134
+ }
2135
+ return context.checker.primitiveAt(url);
2136
+ }
2023
2137
  async function fetchRequest(context, call, client) {
2024
2138
  const [url, options] = call.arguments;
2025
2139
  const kind = url === undefined ? undefined : await client(call.expression);
2026
2140
  if (url === undefined || kind === undefined)
2027
2141
  return;
2028
2142
  const input = await heldAt(context, url);
2029
- const method = input === "unseen" || typeof input === "object" && urlText(context.ts, input.node) ? await declaredMethod(context, options, "GET") : undefined;
2143
+ const method = await urlWithoutMethod(context, url, input) ? await declaredMethod(context, options, "GET") : undefined;
2030
2144
  const parts = await urlParts(context, url);
2031
2145
  const target = kind === "ofetch" ? withBase(await optionsBase(context, options, []), parts) : parts;
2032
2146
  return { ...method === undefined ? {} : { method }, ...requestUrl(target) };
@@ -2065,9 +2179,12 @@ var SHORTHAND = new Map([
2065
2179
  ["patch", "PATCH"],
2066
2180
  ["delete", "DELETE"],
2067
2181
  ["head", "HEAD"],
2068
- ["options", "OPTIONS"]
2182
+ ["options", "OPTIONS"],
2183
+ ["postForm", "POST"],
2184
+ ["putForm", "PUT"],
2185
+ ["patchForm", "PATCH"]
2069
2186
  ]);
2070
- var WITH_BODY = new Set(["post", "put", "patch"]);
2187
+ var WITH_BODY = new Set(["post", "put", "patch", "postForm", "putForm", "patchForm"]);
2071
2188
  async function isAxios(context, node) {
2072
2189
  const origin = await importOrigin(context, node);
2073
2190
  return origin?.module === "axios" && origin.name === "default";
@@ -2093,20 +2210,25 @@ async function axiosClient(context, node) {
2093
2210
  return;
2094
2211
  return await isAxios(context, node) ? defaultClient(context) : createdClient(context, node);
2095
2212
  }
2213
+ async function memberRequest(context, receiver, member, call) {
2214
+ const method = SHORTHAND.get(member);
2215
+ if (method === undefined && member !== "request")
2216
+ return;
2217
+ const client = await axiosClient(context, receiver);
2218
+ if (client === undefined)
2219
+ return;
2220
+ const [first, second, third] = call.arguments;
2221
+ if (method !== undefined && first !== undefined) {
2222
+ return sent(context, client, await urlParts(context, first), method, WITH_BODY.has(member) ? third : second);
2223
+ }
2224
+ return member === "request" ? configRequest(context, client, first) : undefined;
2225
+ }
2096
2226
  async function axiosRequest(context, call) {
2097
2227
  const { ts } = context;
2098
2228
  const callee = call.expression;
2099
- const [first, second, third] = call.arguments;
2229
+ const [first, second] = call.arguments;
2100
2230
  if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
2101
- const client = await axiosClient(context, callee.expression);
2102
- if (client === undefined)
2103
- return;
2104
- const member = callee.name.text;
2105
- const method = SHORTHAND.get(member);
2106
- if (method !== undefined && first !== undefined) {
2107
- return sent(context, client, await urlParts(context, first), method, WITH_BODY.has(member) ? third : second);
2108
- }
2109
- return member === "request" ? configRequest(context, client, first) : undefined;
2231
+ return memberRequest(context, callee.expression, callee.name.text, call);
2110
2232
  }
2111
2233
  const client = await axiosClient(context, callee);
2112
2234
  if (client === undefined || first === undefined)
@@ -2288,7 +2410,7 @@ function typeMember(scope, member) {
2288
2410
  return name === undefined ? [] : [symbolAt(scope, name.text, name, memberVisibility(scope, member))];
2289
2411
  }
2290
2412
  function typeMembers(k, statement) {
2291
- if (is2(statement, k.ClassDeclaration) || is2(statement, k.InterfaceDeclaration))
2413
+ if (is2(statement, k.ClassDeclaration) || is2(statement, k.ClassExpression) || is2(statement, k.InterfaceDeclaration))
2292
2414
  return statement.members;
2293
2415
  return statement.kind === k.EnumDeclaration ? [] : undefined;
2294
2416
  }
@@ -2317,23 +2439,35 @@ function declarationsIn(scope, statements) {
2317
2439
  return [...functionDeclarations(scope, statement), ...typeDeclaration(scope, statement)];
2318
2440
  });
2319
2441
  }
2442
+ function listedExportNames(ts, source) {
2443
+ return new Set(source.statements.flatMap((statement) => listedExports(ts.SyntaxKind, statement)));
2444
+ }
2320
2445
  function outlineSource(ts, source, context) {
2321
2446
  const k = ts.SyntaxKind;
2322
- const exported = new Set([...source.statements.flatMap((statement) => listedExports(k, statement)), ...context.exported ?? []]);
2447
+ const exported = new Set([...listedExportNames(ts, source), ...context.exported ?? []]);
2323
2448
  const scope = { k, source, symbols: context.symbols, exported, topLevelPrivate: context.topLevelPrivate ?? false };
2324
2449
  return declarationsIn(scope, source.statements);
2325
2450
  }
2451
+ function outlineClassExpression(ts, source, value, context) {
2452
+ if (value.kind !== ts.SyntaxKind.ClassExpression)
2453
+ return [];
2454
+ const scope = {
2455
+ k: ts.SyntaxKind,
2456
+ source,
2457
+ symbols: context.symbols,
2458
+ exported: new Set(context.exported ?? []),
2459
+ topLevelPrivate: context.topLevelPrivate ?? false
2460
+ };
2461
+ return typeDeclaration(scope, value);
2462
+ }
2326
2463
 
2327
2464
  // plugins/scanners/javascript/src/outline.ts
2328
2465
  var MODULE_EXTENSION = /\.(?:mjs|cjs)$/;
2329
- function isExportsReference(node) {
2330
- if (ts5.isIdentifier(node))
2331
- return node.text === "exports";
2332
- return ts5.isPropertyAccessExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
2333
- }
2334
2466
  function assignedNames(value) {
2335
2467
  if (ts5.isIdentifier(value))
2336
2468
  return [value.text];
2469
+ if (ts5.isClassExpression(value) && value.name)
2470
+ return [value.name.text];
2337
2471
  if (!ts5.isObjectLiteralExpression(value))
2338
2472
  return [];
2339
2473
  return value.properties.flatMap((property) => {
@@ -2342,29 +2476,31 @@ function assignedNames(value) {
2342
2476
  return ts5.isPropertyAssignment(property) && ts5.isIdentifier(property.initializer) ? [property.initializer.text] : [];
2343
2477
  });
2344
2478
  }
2345
- function commonJsExport2(statement) {
2346
- if (!ts5.isExpressionStatement(statement))
2347
- return;
2348
- const assignment = statement.expression;
2349
- if (!ts5.isBinaryExpression(assignment) || assignment.operatorToken.kind !== ts5.SyntaxKind.EqualsToken)
2350
- return;
2351
- const { left } = assignment;
2352
- const target = ts5.isPropertyAccessExpression(left) && isExportsReference(left.expression) ? left.expression : left;
2353
- return isExportsReference(target) ? assignment : undefined;
2354
- }
2355
2479
  function publishedBy(assignment) {
2356
- const { left, right } = assignment;
2357
- if (ts5.isPropertyAccessExpression(left) && isExportsReference(left.expression)) {
2480
+ const { left } = assignment;
2481
+ const right = assignedValue(assignment.right);
2482
+ if (ts5.isPropertyAccessExpression(left) && !(ts5.isIdentifier(left.expression) && left.expression.text === "module" && left.name.text === "exports")) {
2358
2483
  return ts5.isIdentifier(right) ? [right.text] : [];
2359
2484
  }
2360
2485
  return assignedNames(right);
2361
2486
  }
2362
- function statesEcmaScriptModule(source) {
2363
- return source.statements.some((statement) => ts5.isImportDeclaration(statement) || ts5.isExportDeclaration(statement) || ts5.isExportAssignment(statement) || ts5.canHaveModifiers(statement) && (ts5.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword));
2487
+ function usesRequire(source) {
2488
+ let found = false;
2489
+ const visit = (node) => {
2490
+ if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "require")
2491
+ found = true;
2492
+ else if (!found)
2493
+ ts5.forEachChild(node, visit);
2494
+ };
2495
+ ts5.forEachChild(source, visit);
2496
+ return found;
2497
+ }
2498
+ function statesModuleBoundary(source) {
2499
+ return usesRequire(source) || source.statements.some((statement) => ts5.isImportDeclaration(statement) || ts5.isExportDeclaration(statement) || ts5.isExportAssignment(statement) || ts5.canHaveModifiers(statement) && (ts5.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword));
2364
2500
  }
2365
2501
  function publishedNames(file, source) {
2366
- const exports = source.statements.flatMap((statement) => commonJsExport2(statement) ?? []);
2367
- if (exports.length > 0 || MODULE_EXTENSION.test(file) || statesEcmaScriptModule(source)) {
2502
+ const exports = source.statements.flatMap((statement) => commonJsExport(statement) ?? []);
2503
+ if (exports.length > 0 || MODULE_EXTENSION.test(file) || statesModuleBoundary(source)) {
2368
2504
  return exports.flatMap(publishedBy);
2369
2505
  }
2370
2506
  return topLevelDeclarations(source).map((declaration) => declaration.name);
@@ -2375,7 +2511,13 @@ async function readJavaScriptOutline(repositoryRoot, references) {
2375
2511
  const fileName = path.join(repositoryRoot, reference.file);
2376
2512
  const text = await readFile(fileName, "utf8");
2377
2513
  const source = ts5.createSourceFile(fileName, text, ts5.ScriptTarget.Latest, true);
2378
- const declarations = outlineSource(ts5, source, { symbols: reference.symbols, exported: publishedNames(reference.file, source) });
2514
+ const exported = publishedNames(reference.file, source);
2515
+ const classes = source.statements.flatMap((statement) => {
2516
+ const assignment = commonJsExport(statement);
2517
+ const value = assignment && assignedValue(assignment.right);
2518
+ return value && ts5.isClassExpression(value) ? outlineClassExpression(ts5, source, value, { symbols: reference.symbols, exported }) : [];
2519
+ });
2520
+ const declarations = [...outlineSource(ts5, source, { symbols: reference.symbols, exported }), ...classes].sort((left, right) => left.line - right.line);
2379
2521
  if (declarations.length > 0)
2380
2522
  files.push({ file: reference.file, declarations });
2381
2523
  }
@@ -2384,67 +2526,268 @@ async function readJavaScriptOutline(repositoryRoot, references) {
2384
2526
 
2385
2527
  // plugins/scanners/javascript/src/sources.ts
2386
2528
  import { readFile as readFile2 } from "fs/promises";
2387
- import path3 from "path";
2388
-
2389
- // plugins/scanners/projects.ts
2390
- import { execFile } from "child_process";
2391
- import { existsSync } from "fs";
2392
2529
  import path2 from "path";
2393
- import { promisify } from "util";
2394
- var execute = promisify(execFile);
2395
- var excluded = new Set([
2396
- ".git",
2397
- "node_modules",
2398
- "vendor",
2399
- "target",
2400
- "dist",
2401
- "build",
2402
- "bin",
2403
- "obj",
2404
- ".gradle",
2405
- ".angular",
2406
- "coverage",
2407
- "generated",
2408
- "groma",
2409
- ".groma"
2410
- ]);
2411
- async function repositoryFiles(root, matches) {
2412
- const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
2413
- return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path2.join(root, file))))].sort();
2530
+ var SOURCE = /\.(?:js|mjs|cjs|jsx)$/;
2531
+ function javaScriptFiles(files) {
2532
+ return files.filter((file) => SOURCE.test(file));
2414
2533
  }
2415
- function projectFiles(root, matches) {
2416
- return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
2534
+ async function javaScriptSources(root, files) {
2535
+ const sources = [];
2536
+ for (const file of javaScriptFiles(files))
2537
+ sources.push({ file, text: await readFile2(path2.join(root, file), "utf8") });
2538
+ return sources;
2417
2539
  }
2418
2540
 
2419
- // plugins/scanners/javascript/src/sources.ts
2420
- var SOURCE = /\.(?:js|mjs|cjs|jsx)$/;
2421
- var MINIFIED_NAME = /\.min\.(?:js|mjs|cjs|jsx)$/;
2422
- var MINIFIED_LINE_LENGTH = 500;
2423
- function isJavaScriptFile(file) {
2424
- return SOURCE.test(file);
2541
+ // plugins/scanners/entry-points/javascript.ts
2542
+ import { readFile as readFile3 } from "fs/promises";
2543
+ import path4 from "path";
2544
+
2545
+ // plugins/scanners/workspace-packages.ts
2546
+ import path3 from "path";
2547
+ function inRootDir(file, outputs) {
2548
+ const output = outputs.find(({ outDir }) => file.startsWith(`${outDir}/`));
2549
+ return output === undefined ? undefined : path3.posix.join(output.rootDir, file.slice(output.outDir.length + 1));
2425
2550
  }
2426
- function hasMinifiedName(file) {
2427
- return MINIFIED_NAME.test(path3.posix.basename(file));
2551
+ function sourceOf(file, outputs, files) {
2552
+ const stem = inRootDir(file, outputs)?.replace(/\.(?:d\.[cm]?ts|[cm]?jsx?)$/, "");
2553
+ return stem === undefined ? undefined : [".ts", ".tsx"].map((extension) => `${stem}${extension}`).find((candidate) => files.has(candidate));
2428
2554
  }
2429
- function isMinifiedText(text) {
2430
- const code = text.trimEnd();
2431
- return code.length / code.split(`
2432
- `).length >= MINIFIED_LINE_LENGTH;
2555
+ var conditions = new Set(["types", "import", "default", "require"]);
2556
+
2557
+ // plugins/scanners/entry-points/javascript.ts
2558
+ function packageFor(file, packages) {
2559
+ let directory = path4.posix.dirname(file);
2560
+ while (directory !== "." && !packages.has(directory))
2561
+ directory = path4.posix.dirname(directory);
2562
+ return directory;
2563
+ }
2564
+ function declaredBins(declaration, manifest) {
2565
+ const bins = typeof manifest.bin === "string" ? { [String(manifest.name ?? "main")]: manifest.bin } : manifest.bin;
2566
+ if (!bins || typeof bins !== "object")
2567
+ return [];
2568
+ return Object.entries(bins).flatMap(([name, target]) => typeof target === "string" ? [{ declaration, name, file: path4.posix.join(path4.posix.dirname(declaration), target) }] : []);
2569
+ }
2570
+ var RUNTIMES = new Set(["node", "bun", "tsx", "ts-node"]);
2571
+ var VALUE_OPTIONS = new Set(["-r", "--require", "--import", "--tsconfig"]);
2572
+ var SCRIPT = /^[\w./-]+\.[cm]?[jt]sx?$/;
2573
+ function runtimeAt(words) {
2574
+ let index = words[0] === "cross-env" ? 1 : 0;
2575
+ while (/^[A-Za-z_]\w*=/.test(words[index] ?? ""))
2576
+ index += 1;
2577
+ return index;
2433
2578
  }
2434
- async function javaScriptSources(root) {
2435
- const files = await projectFiles(root, (file) => isJavaScriptFile(file) && !hasMinifiedName(file));
2436
- const sources = [];
2437
- for (const file of files) {
2438
- const text = await readFile2(path3.join(root, file), "utf8");
2439
- if (!isMinifiedText(text))
2440
- sources.push({ file, text });
2579
+ function commandScript(command) {
2580
+ const words = command.trim().split(/\s+/);
2581
+ let index = runtimeAt(words);
2582
+ if (!RUNTIMES.has(words[index] ?? ""))
2583
+ return;
2584
+ let subcommand = true;
2585
+ for (index += 1;index < words.length; index += 1) {
2586
+ const word = words[index];
2587
+ if (VALUE_OPTIONS.has(word))
2588
+ index += 1;
2589
+ else if (subcommand && (word === "run" || word === "watch"))
2590
+ subcommand = false;
2591
+ else if (!word.startsWith("-"))
2592
+ return SCRIPT.test(word) ? word : undefined;
2593
+ }
2594
+ return;
2595
+ }
2596
+ function declaredScripts(declaration, scripts) {
2597
+ return Object.entries(scripts ?? {}).flatMap(([name, script]) => {
2598
+ if (typeof script !== "string")
2599
+ return [];
2600
+ const commands = /["'`]/.test(script) ? [script] : script.split(/\s+&&\s+/);
2601
+ return commands.flatMap((command) => {
2602
+ const file = commandScript(command);
2603
+ return file === undefined ? [] : [{ declaration, name, file: path4.posix.join(path4.posix.dirname(declaration), file) }];
2604
+ });
2605
+ });
2606
+ }
2607
+ var isString = (value) => typeof value === "string";
2608
+ function buildEntry(declaration, workspace, name, project) {
2609
+ const build = (project.architect ?? project.targets)?.build;
2610
+ const main = build?.options?.browser ?? build?.options?.main;
2611
+ if (!isString(main))
2612
+ return [];
2613
+ const replacements = Object.values(build?.configurations ?? {}).flatMap((configuration) => Array.isArray(configuration.fileReplacements) ? configuration.fileReplacements.map((replacement) => replacement?.with) : []);
2614
+ const at = (file) => path4.posix.join(workspace, file);
2615
+ return [{ declaration, name, file: at(main), compiled: [build?.options?.polyfills, ...replacements].flat().filter(isString).map(at) }];
2616
+ }
2617
+ function angularEntries(declaration, source) {
2618
+ const projects = JSON.parse(source).projects ?? {};
2619
+ return Object.entries(projects).flatMap(([name, project]) => buildEntry(declaration, path4.posix.dirname(declaration), name, project));
2620
+ }
2621
+ function nxEntries(declaration, source, workspaces) {
2622
+ const workspace = packageFor(declaration, workspaces);
2623
+ if (!workspaces.has(workspace))
2624
+ return [];
2625
+ const project = JSON.parse(source);
2626
+ return buildEntry(declaration, workspace, project.name ?? path4.posix.basename(path4.posix.dirname(declaration)), project);
2627
+ }
2628
+ function browserEntries(declaration, source, packages) {
2629
+ const scripts = [...source.matchAll(/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi)];
2630
+ const folder = path4.posix.dirname(declaration);
2631
+ const page = path4.posix.basename(declaration, ".html");
2632
+ const name = page === "index" && folder !== "." ? path4.posix.basename(folder) : page;
2633
+ return scripts.flatMap((match) => {
2634
+ const src = match[1];
2635
+ if (/^(?:[a-z]+:|\/\/)/i.test(src))
2636
+ return [];
2637
+ const base = src.startsWith("/") ? packageFor(declaration, packages) : folder;
2638
+ return [{ file: path4.posix.join(base, src.replace(/^\//, "")), declaration, name }];
2639
+ });
2640
+ }
2641
+ function reachedFiles(entry, compiled, imports, packages, units) {
2642
+ const owner = packageFor(entry, packages), found = new Set;
2643
+ const pending = [entry, ...compiled];
2644
+ while (pending.length) {
2645
+ const file = pending.pop();
2646
+ if (found.has(file) || packageFor(file, packages) !== owner)
2647
+ continue;
2648
+ found.add(file);
2649
+ pending.push(...imports.get(file) ?? []);
2441
2650
  }
2442
- return sources;
2651
+ for (const unit of units)
2652
+ if (found.has(unit.primary))
2653
+ for (const file of unit.files)
2654
+ found.add(file);
2655
+ return found;
2656
+ }
2657
+ async function declaredEntries(root, declarations, packages, workspaces) {
2658
+ const entries = [];
2659
+ for (const file of declarations) {
2660
+ const text = await readFile3(path4.join(root, file), "utf8");
2661
+ const name = path4.posix.basename(file);
2662
+ if (name === "package.json") {
2663
+ const manifest = JSON.parse(text);
2664
+ entries.push(...declaredBins(file, manifest), ...declaredScripts(file, manifest.scripts));
2665
+ } else if (name === "angular.json")
2666
+ entries.push(...angularEntries(file, text));
2667
+ else if (name === "project.json")
2668
+ entries.push(...nxEntries(file, text, workspaces));
2669
+ else if (file.endsWith(".html"))
2670
+ entries.push(...browserEntries(file, text, packages));
2671
+ }
2672
+ return entries;
2673
+ }
2674
+ function sourced(entry, files, outputs = []) {
2675
+ const file = sourceOf(entry.file, outputs, files);
2676
+ return file === undefined ? entry : { ...entry, file };
2677
+ }
2678
+ async function withJavaScriptEntries(root, observation, inputs, files) {
2679
+ if (!observation)
2680
+ return;
2681
+ const inventory = files.filter((file) => ["package.json", "angular.json", "project.json", "nx.json"].includes(path4.posix.basename(file)) || file.endsWith(".html"));
2682
+ const workspaces = new Set(inventory.filter((file) => path4.posix.basename(file) === "nx.json").map((file) => path4.posix.dirname(file)));
2683
+ const packages = new Set(inventory.filter((file) => path4.posix.basename(file) === "package.json").map((file) => path4.posix.dirname(file)));
2684
+ const active = new Set(observation.files.map((file) => packageFor(file.file, packages)));
2685
+ const declarations = inventory.filter((file) => path4.posix.basename(file) !== "nx.json" && active.has(packageFor(file, packages)));
2686
+ const entries = [...inputs.entries, ...await declaredEntries(root, declarations, packages, workspaces)];
2687
+ const inventoryFiles = [...observation.files];
2688
+ const visible = new Set(inventoryFiles.map((file) => file.file));
2689
+ const readable = new Set(files);
2690
+ const entryPoints = entries.map((entry) => sourced(entry, readable, inputs.buildOutputs)).flatMap(({ compiled = [], ...entry }) => {
2691
+ if (!inputs.imports.has(entry.file))
2692
+ return [];
2693
+ const reached = reachedFiles(entry.file, compiled, inputs.imports, packages, observation.sourceUnits ?? []);
2694
+ const members = observation.files.filter((file) => reached.has(file.file));
2695
+ if (!members.length)
2696
+ return [];
2697
+ if (!visible.has(entry.file)) {
2698
+ inventoryFiles.push({ file: entry.file, symbols: [], roots: [...new Set(members.flatMap((file) => file.roots))] });
2699
+ visible.add(entry.file);
2700
+ }
2701
+ return [{ ...entry, files: [...new Set([entry.file, ...members.map((file) => file.file)])] }];
2702
+ });
2703
+ return createScanObservation({ ...observation, files: inventoryFiles, entryPoints });
2704
+ }
2705
+
2706
+ // plugins/scanners/javascript/src/entries.ts
2707
+ import path6 from "path";
2708
+ import ts6 from "./typescript.cjs";
2709
+
2710
+ // plugins/scanners/entry-points/source.ts
2711
+ import path5 from "path";
2712
+ var relative = (root, file) => path5.relative(root, path5.resolve(root, file)).split(path5.sep).join("/");
2713
+ async function text2(context, node) {
2714
+ if (!node)
2715
+ return;
2716
+ const parts = await urlParts(context, node);
2717
+ return parts.every((part) => part.kind === "text") ? parts.map((part) => part.kind === "text" ? part.text : "").join("") : undefined;
2718
+ }
2719
+ async function entryPath(root, context, node) {
2720
+ const { ts } = context;
2721
+ const direct = await text2(context, node);
2722
+ if (direct !== undefined)
2723
+ return relative(root, direct);
2724
+ if (!ts.isCallExpression(node))
2725
+ return;
2726
+ const origin = await importOrigin(context, node.expression);
2727
+ if (origin?.name !== "fileURLToPath" || !["node:url", "url"].includes(origin.module))
2728
+ return;
2729
+ const url = node.arguments[0];
2730
+ if (!url || !ts.isNewExpression(url) || !ts.isIdentifier(url.expression) || url.expression.text !== "URL" || !await runtimeGlobal(context, url.expression))
2731
+ return;
2732
+ const base = url.arguments?.[1];
2733
+ if (!base || !ts.isPropertyAccessExpression(base) || base.name.text !== "url" || !ts.isMetaProperty(base.expression))
2734
+ return;
2735
+ const value = await text2(context, url.arguments?.[0]);
2736
+ return value === undefined ? undefined : relative(root, path5.resolve(root, path5.dirname(node.getSourceFile().fileName), value));
2737
+ }
2738
+ async function buildEntries(root, context, call) {
2739
+ const { ts } = context;
2740
+ const callee = call.expression;
2741
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "build" || !ts.isIdentifier(callee.expression) || callee.expression.text !== "Bun" || !await runtimeGlobal(context, callee.expression))
2742
+ return [];
2743
+ const fields = await objectEntries(context, call.arguments[0]);
2744
+ if (!fields)
2745
+ return [];
2746
+ const compile = fields.get("compile"), entrypoints = fields.get("entrypoints");
2747
+ const executable = compile && await heldAt(context, compile);
2748
+ const native = typeof executable === "object" && (executable.node.kind === ts.SyntaxKind.TrueKeyword || ts.isObjectLiteralExpression(executable.node));
2749
+ if (!native && await literalText(context, fields.get("target")) !== "browser")
2750
+ return [];
2751
+ const array = entrypoints && await heldAt(context, entrypoints);
2752
+ if (typeof array !== "object" || !ts.isArrayLiteralExpression(array.node))
2753
+ return [];
2754
+ const declaration = relative(root, call.getSourceFile().fileName);
2755
+ const files = await Promise.all(array.node.elements.map((node) => entryPath(root, context, node)));
2756
+ return files.flatMap((file) => file === undefined ? [] : [{ file, declaration, name: path5.posix.basename(file).replace(/\.[^.]+$/, "") }]);
2757
+ }
2758
+ async function sourceBuildEntries(root, ts, checker, sources) {
2759
+ const context = urlContext(ts, checker, sources);
2760
+ const calls = [];
2761
+ const visit = (node) => {
2762
+ if (ts.isCallExpression(node))
2763
+ calls.push(node);
2764
+ ts.forEachChild(node, visit);
2765
+ };
2766
+ for (const source of sources)
2767
+ visit(source);
2768
+ return (await Promise.all(calls.map((call) => buildEntries(root, context, call)))).flat();
2769
+ }
2770
+
2771
+ // plugins/scanners/javascript/src/entries.ts
2772
+ async function javaScriptEntries(root, sources) {
2773
+ const files = new Set(sources.map((source) => source.fileName));
2774
+ const imports = new Map(sources.map((source) => {
2775
+ const targets = ts6.preProcessFile(source.text, true, true).importedFiles.flatMap((imported) => {
2776
+ if (!imported.fileName.startsWith("."))
2777
+ return [];
2778
+ const base = path6.posix.normalize(path6.posix.join(path6.posix.dirname(source.fileName), imported.fileName));
2779
+ const candidates = [base, ...[".js", ".mjs", ".cjs", ".jsx"].flatMap((extension) => [base + extension, `${base}/index${extension}`])];
2780
+ return candidates.find((file) => files.has(file)) ?? [];
2781
+ });
2782
+ return [source.fileName, targets];
2783
+ }));
2784
+ const entries = (await Promise.all(sources.map((source) => sourceBuildEntries(root, ts6, classicChecker(ts6, fileChecker(source)), [source])))).flat();
2785
+ return { imports, entries };
2443
2786
  }
2444
2787
 
2445
2788
  // plugins/scanners/javascript/src/index.ts
2446
2789
  function parse(source) {
2447
- return ts6.createSourceFile(source.file, source.text, ts6.ScriptTarget.Latest, true);
2790
+ return ts7.createSourceFile(source.file, source.text, ts7.ScriptTarget.Latest, true);
2448
2791
  }
2449
2792
  var SLOPPY_MODE_ONLY = new Set([1121, 1487, 1488, 1489]);
2450
2793
  function parseWarning(source) {
@@ -2457,23 +2800,19 @@ function parseWarning(source) {
2457
2800
  code: "JAVASCRIPT_SOURCE_INVALID",
2458
2801
  file: source.fileName,
2459
2802
  line: source.getLineAndCharacterOfPosition(first.start).line + 1,
2460
- message: `The file does not parse, so it contributes no evidence: ${ts6.flattenDiagnosticMessageText(first.messageText, " ")}`
2803
+ message: `The file does not parse, so it contributes no evidence: ${ts7.flattenDiagnosticMessageText(first.messageText, " ")}`
2461
2804
  };
2462
2805
  }
2463
2806
  var src_default = {
2464
2807
  id: "javascript",
2465
- watch: {
2466
- include: ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx"],
2467
- exclude: ["**/*.min.js", "**/*.min.mjs", "**/*.min.cjs", "**/*.min.jsx"]
2468
- },
2469
- listSourceFiles: async (root) => (await javaScriptSources(root)).map((source) => source.file),
2470
- async checkReadiness(root) {
2471
- if (!(await javaScriptSources(root)).length) {
2472
- throw new Error("javascript: No authored JavaScript source files were found in the Git repository.");
2808
+ listSourceFiles: async (_root, _settings, candidates) => javaScriptFiles(candidates),
2809
+ async checkReadiness(_root, _settings, files) {
2810
+ if (!javaScriptFiles(files).length) {
2811
+ throw new Error("javascript: No JavaScript source files were found in the Git repository.");
2473
2812
  }
2474
2813
  },
2475
- async scan(root) {
2476
- const sources = await javaScriptSources(root);
2814
+ async scan(root, _settings, files) {
2815
+ const sources = await javaScriptSources(root, files);
2477
2816
  if (!sources.length)
2478
2817
  return;
2479
2818
  const parsed = sources.map(parse);
@@ -2484,9 +2823,9 @@ var src_default = {
2484
2823
  return { file: source.fileName, evidence, facts: await javaScriptHttpFacts(source, evidence) };
2485
2824
  }));
2486
2825
  const symbols = new Map(scanned.map(({ file, evidence }) => [file, evidence.symbols]));
2487
- return createScanObservation({
2488
- scanner: { id: "javascript", technology: "javascript", engine: "typescript-sdk", engineVersion: ts6.version },
2489
- roots: [{ id: "javascript-source", kind: "source-group", name: path4.basename(root) }],
2826
+ return withJavaScriptEntries(root, createScanObservation({
2827
+ scanner: { id: "javascript", technology: "javascript", engine: "typescript-sdk", engineVersion: ts7.version },
2828
+ roots: [{ id: "javascript-source", kind: "source-group", name: path7.basename(root) }],
2490
2829
  files: parsed.map(({ fileName: file }) => ({ file, symbols: symbols.get(file) ?? [], roots: ["javascript-source"] })),
2491
2830
  operations: scanned.flatMap(({ evidence }) => evidence.operations),
2492
2831
  invocations: scanned.flatMap(({ evidence }) => evidence.invocations),
@@ -2500,7 +2839,7 @@ var src_default = {
2500
2839
  },
2501
2840
  ...warnings.filter((warning) => warning !== undefined)
2502
2841
  ]
2503
- });
2842
+ }), await javaScriptEntries(root, readable), files);
2504
2843
  },
2505
2844
  readCodeStructure: readJavaScriptOutline
2506
2845
  };