@groma/scanner-javascript 0.1.0 → 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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # JavaScript scanner
2
2
 
3
- The JavaScript scanner reads tracked and unignored `.js`, `.mjs`, `.cjs` and
4
- `.jsx` files, covering ECMAScript modules, CommonJS modules and browser scripts.
3
+ The JavaScript scanner reads the `.js`, `.mjs`, `.cjs` and `.jsx` files
4
+ groma.md hands it, covering ECMAScript modules, CommonJS modules and browser scripts.
5
5
  It does not require Node.js, installed packages, a bundler, a project build or a
6
6
  `tsconfig.json`. Discovery matches JavaScript source directly, even without a
7
7
  package manifest.
@@ -17,7 +17,7 @@ groma scanner add /absolute/path/to/groma/plugins/scanners/javascript/dist/packa
17
17
  groma scan
18
18
  ```
19
19
 
20
- The package bundles the TypeScript 6.0.3 compiler, whose parser reads
20
+ The package bundles the TypeScript 6.0 compiler, whose parser reads
21
21
  JavaScript, including JSX in `.js` and `.jsx` files, and includes its license
22
22
  notices. Every file is parsed on its own, so no project configuration, module
23
23
  resolution or other file changes what a file reports. The compiler's declaration
@@ -31,28 +31,21 @@ TypeScript-style type annotations, as in many Flow-typed files, still parse, and
31
31
  octal literals and escapes that only strict mode rejects, such as `0755`, are not
32
32
  parse errors.
33
33
 
34
- ## Excluded files
34
+ ## Included and excluded files
35
35
 
36
- Minified output is not authored source, so it is left out of the scan and the
37
- source outline:
36
+ The package declares the default
37
+ [include and exclude lists](../index.md#selecting-source-files). It includes
38
+ the four source extensions and the `package.json`, HTML, `project.json` and
39
+ `nx.json` files the entry reader reads. It excludes names that state minified
40
+ output, such as `jquery-ui.min.js`, and `node_modules`, `dist`, `build` and
41
+ `coverage` folders. The scan and the readiness check read only the files the
42
+ lists select.
38
43
 
39
- - a name that states minified output, such as `jquery-ui.min.js`;
40
- - text whose lines average 500 characters or more, the shape of a bundle a build
41
- produced under an ordinary name such as `vendor.js`.
44
+ A bundle under an ordinary name, such as `vendor.js`, is read like any other
45
+ source; a project that keeps it out adds a pattern to `scanners.json`.
42
46
 
43
- The shared Git boundary omits ignored files and dependency or build directories
44
- such as `node_modules`, `vendor`, `dist` and `build`. Core applies the source
45
- exclusions configured in `scanners.json`.
46
-
47
- Test sources are treated like any other JavaScript source, as in the PHP and
48
- Swift scanners: a project excludes them explicitly in `scanners.json`. The
49
- TypeScript scanner instead omits `test` directories and `.test` and `.spec` names
50
- by default.
51
-
52
- Discovery counts any `.js`, `.mjs`, `.cjs` or `.jsx` name, minified files
53
- included, because it reads file names without opening them. A repository whose
54
- only JavaScript is minified can therefore be recommended this scanner, and
55
- readiness then reports that no authored JavaScript source was found.
47
+ Test sources are treated like any other JavaScript source: the defaults name
48
+ none, so a project that keeps them out adds a pattern to `scanners.json`.
56
49
 
57
50
  TypeScript sources belong to the [TypeScript scanner](../typescript/index.md)
58
51
  and single-file components to the [Vue scanner](../vue/index.md). This scanner
@@ -66,6 +59,8 @@ and methods and constructors with a body produce operation evidence with UTF-16
66
59
  source offsets.
67
60
  Code outside every function is the module's own work, so a browser script's
68
61
  top-level calls keep an operation to belong to, without inventing a function.
62
+ Calls inside accessors belong to an anonymous accessor operation; accessors are
63
+ not compared for duplicate logic.
69
64
 
70
65
  Calls remain unresolved. One parsed file proves no call target: an imported name,
71
66
  a `require` result and a method on a value are all decided elsewhere, so core
@@ -77,6 +72,13 @@ JavaScript file belongs to one C4 runtime boundary. Core owns architecture
77
72
  identity and curation, and repeated scans preserve one physical owner and the
78
73
  authored architecture. JavaScript edits and new files refresh through the shared
79
74
  scanner watcher.
75
+ Each literal local `node`, `bun`, `tsx` or `ts-node` command in an unquoted
76
+ `&&`-chained package script can supply an execution entry. Environment
77
+ assignments or `cross-env` may come before the runtime, and runtime options and
78
+ a `run` or `watch` subcommand before the script; the first remaining word must
79
+ name the script, so a preloaded module or `bun build` supplies none. An HTML
80
+ page's script supplies one too, named after the page, or after its folder for
81
+ an `index.html`.
80
82
 
81
83
  ## Source outline
82
84
 
@@ -86,7 +88,8 @@ parses the file alone and lists what it declares at the top level:
86
88
 
87
89
  - functions, and arrow functions or function expressions assigned directly to a
88
90
  `const`, `let` or `var` name;
89
- - classes, with every method and the constructor as members.
91
+ - classes, including a named class expression assigned to `module.exports`,
92
+ with every method and the constructor as members.
90
93
 
91
94
  Fields, accessors, and declarations inside a function, method or block are not
92
95
  listed. A declaration is marked as an entry when the component's Code names it,
@@ -96,12 +99,12 @@ Visibility follows how the file publishes a name:
96
99
 
97
100
  | Value | JavaScript |
98
101
  | --- | --- |
99
- | `public` | An `export`, a name in the file's own `export { name }` list or `export default name`, a name a CommonJS `module.exports` or `exports.name` assignment publishes, and every top-level declaration of a file that states no module boundary, because those names are globals |
102
+ | `public` | An `export`, a name in the file's own `export { name }` list or `export default name`, a name a CommonJS `module.exports` or `exports.name` assignment publishes, including through a chained assignment, and every top-level declaration of a file that states no module boundary, because those names are globals |
100
103
  | `private` | Every other top-level declaration, and a `#name` member |
101
104
 
102
105
  A `.mjs` or `.cjs` file is a module whatever it contains, so its unpublished
103
106
  declarations stay private. A `.js` file is a module when it states an `import`,
104
- an `export` or a CommonJS export; otherwise it is a browser script whose
107
+ an `export`, a CommonJS export or a `require` call; otherwise it is a browser script whose
105
108
  declarations any other script on the page may use. Members are `public` unless
106
109
  their name is private.
107
110
 
@@ -125,8 +128,8 @@ that file states it and a value from another file is one the scanner cannot see.
125
128
 
126
129
  | Construct | Reported |
127
130
  | --- | --- |
128
- | `fetch(url, init)`, including a `node-fetch` default import | Request; a literal `method` gives the method, no options means `GET`, and options the scanner cannot read, or an input that is not a URL, such as a `Request`, leave it out |
129
- | `axios.get`, `.post`, `.put`, `.patch`, `.delete`, `.head`, `.options` | Request with that method |
131
+ | `fetch(url, init)`, including a `node-fetch` default import or a named `undici` import | Request; a literal `method` gives the method, no options means `GET`, and options the scanner cannot read, or an input that is neither URL text nor typed as a primitive such as a string, for example a `Request`, leave it out |
132
+ | `axios.get`, `.post`, `.put`, `.patch`, `.delete`, `.head`, `.options`, `.postForm`, `.putForm`, `.patchForm` | Request with that method; form helpers use their matching HTTP method |
130
133
  | `axios(config)`, `axios.request(config)` | Request from the config's `url`; its `method`, else the client's, else `GET` |
131
134
  | `axios.create(config)` instances | Request whose path follows the config's `baseURL`, and whose method defaults to the config's |
132
135
  | `$.get`, `$.post`, `$.getJSON`, `$.getScript` | Request with that helper's method |
@@ -209,21 +212,29 @@ The [producer decisions](../evidence.md#producer-checklist) for this ecosystem:
209
212
  7. **Constrained segments.** Route patterns are literal text, `:name`, `:name?`,
210
213
  Express 5's trailing optional group `{/:name}`, and a trailing `*`, `*name`,
211
214
  `(.*)` or `:name(.*)` catch-all, which needs at least one segment because a
212
- request path cannot tell `/files` from `/files/`; Hono's trailing `*` also
213
- matches the path without it. A pattern parameter, such as `:id(\d+)`, and
215
+ request path cannot tell `/files` from `/files/`; Hono's trailing `*` and
216
+ Express 5's trailing `{/*name}` or `/{*name}` also match the path without it.
217
+ A pattern parameter, such as `:id(\d+)`, and
214
218
  text mixed with a placeholder, such as `talk-:id`, are constrained
215
219
  parameters. A pattern that may span segments or that the scanner cannot
216
220
  state, such as a regular expression, another optional group or a catch-all
217
221
  that is not last, becomes a constrained optional catch-all in place of itself
218
- and the rest of the route, so no route is omitted.
222
+ and the rest of the route, so no route is omitted; the whole segments before
223
+ it stand.
219
224
  8. **Registration order.** Express, Hono and Koa routers take the first
220
225
  registered match, so their endpoints carry `order`: the application, named by
221
226
  the file that creates it and the variable that holds it, such as
222
- `server/app.js#app`, and a position. The routing model has six rules:
227
+ `server/app.js#app`, and a position. Same-named local applications in one
228
+ file also carry their declaration position to keep their order separate.
229
+ The routing model has six rules:
223
230
  1. A registrar's entries are its registrations and the references that hand
224
231
  it off to other code, such as `registerRoutes(app)`. Serving it with
225
232
  `listen`, Node's `createServer(app)` or an imported `serve(app)` registers
226
- nothing. The scanner never sees the files that import this one, which run
233
+ nothing, and neither does a route method called with fewer than two
234
+ arguments, such as the setting read `app.get('env')`. A `use` that only
235
+ adds middleware, which takes no place (rule 5), is no entry either, so
236
+ `if (app.get('env') === 'development') app.use(logger)` leaves the order
237
+ of the other entries known. The scanner never sees the files that import this one, which run
227
238
  after all of it and may add any route, so an export, `export default app`,
228
239
  `export const app` or an assignment to `module.exports` or `exports`,
229
240
  hands the registrar off at the end of its order; a circular require is the
@@ -244,9 +255,9 @@ The [producer decisions](../evidence.md#producer-checklist) for this ecosystem:
244
255
  prefix followed by a constrained optional catch-all, with method `*`
245
256
  unless the call states one, named by the registering operation: a route
246
257
  with a computed path, a mount under a computed prefix, a path mounted to
247
- something other than a recognized router, such as
248
- `app.use('/static', express.static('public'))`, a router the scan cannot
249
- follow, mounted with or without a path, such as
258
+ something other than a recognized router or a function the scan sees,
259
+ such as `app.use('/static', express.static('public'))`, a router the
260
+ scan cannot follow, mounted with or without a path, such as
250
261
  `app.use(require('./routes'))`, a Koa router's `routes()` from another
251
262
  file, or a function imported from one, which the scanner cannot see, a
252
263
  route builder such as `app.route('/reports')`, Hono's `on`, `mount` and
@@ -255,7 +266,11 @@ The [producer decisions](../evidence.md#producer-checklist) for this ecosystem:
255
266
  registrar's root.
256
267
  5. Middleware takes no place: a package's handler, such as `express.json()`,
257
268
  and any handler before or beside a recognized router in one call, as in
258
- `app.use(requireAuth, api)`, which then states no path.
269
+ `app.use(requireAuth, api)`, which then states no path. Hono's `use`
270
+ never mounts a router, so it takes no place with or without a path,
271
+ whatever its handlers. Under a path, an Express or Koa `use` takes none
272
+ when the scan sees that every handler is a function, as in
273
+ `app.use('/api', requireAuth)`.
259
274
  6. Fastify and Bun.serve prefer the most specific route and carry no order; a
260
275
  Fastify plugin, whose routes the scan does not read, blocks its prefix
261
276
  without one.
@@ -263,7 +278,7 @@ The [producer decisions](../evidence.md#producer-checklist) for this ecosystem:
263
278
  ## Validation
264
279
 
265
280
  Independent fixtures cover ECMAScript modules, CommonJS, JSX, browser scripts,
266
- minified files excluded by name and by line length, exact source positions,
281
+ minified names excluded by the default exclusions, exact source positions,
267
282
  unresolved calls, the source outline with its visibility rules, one body that
268
283
  tokenizes identically in JavaScript and in TypeScript, identical and
269
284
  near-duplicate bodies found by `groma lint`, a file that does not parse next to
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) {
@@ -156,6 +156,7 @@ function createScanObservation(input) {
156
156
  roots,
157
157
  files,
158
158
  ...sourceUnits(input.sourceUnits, filePaths),
159
+ ...entryPoints(input.entryPoints, filePaths),
159
160
  ...operationEvidence(input, filePaths),
160
161
  ...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
161
162
  diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
@@ -166,6 +167,29 @@ function createScanObservation(input) {
166
167
  })
167
168
  };
168
169
  }
170
+ function entryPoints(input, paths) {
171
+ if (input === undefined)
172
+ return {};
173
+ const entries = array(input, "entryPoints").map((value) => {
174
+ const entry = object(value, "entry point");
175
+ const files = [...new Set(array(entry.files, "entry point.files").map((file) => string(file, "entry point.file")))].sort();
176
+ for (const file of files) {
177
+ if (!paths.has(file))
178
+ throw new Error(`entry point references unknown file: ${file}`);
179
+ }
180
+ const file = string(entry.file, "entry point.file");
181
+ if (!files.includes(file))
182
+ throw new Error(`entry point must include its own source file: ${file}`);
183
+ return {
184
+ file,
185
+ declaration: string(entry.declaration, "entry point.declaration"),
186
+ name: string(entry.name, "entry point.name"),
187
+ files
188
+ };
189
+ });
190
+ const key = (entry) => compare(entry.file, entry.declaration, entry.name, ...entry.files);
191
+ return { entryPoints: [...new Map(entries.map((entry) => [key(entry), entry])).values()].sort((a, b) => key(a).localeCompare(key(b))) };
192
+ }
169
193
  function sourceUnits(input, paths) {
170
194
  if (input === undefined)
171
195
  return {};
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groma/scanner-javascript",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "JavaScript source declarations, outlines and operation bodies without project dependencies.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -9,6 +9,26 @@
9
9
  "scanner": {
10
10
  "id": "javascript",
11
11
  "entry": "./dist/index.js",
12
+ "include": [
13
+ "**/*.js",
14
+ "**/*.mjs",
15
+ "**/*.cjs",
16
+ "**/*.jsx",
17
+ "**/package.json",
18
+ "**/*.html",
19
+ "**/project.json",
20
+ "**/nx.json"
21
+ ],
22
+ "exclude": [
23
+ "*.min.js",
24
+ "*.min.mjs",
25
+ "*.min.cjs",
26
+ "*.min.jsx",
27
+ "node_modules/",
28
+ "dist/",
29
+ "build/",
30
+ "coverage/"
31
+ ],
12
32
  "discovery": {
13
33
  "technologies": [
14
34
  "javascript"
@@ -28,7 +48,7 @@
28
48
  }
29
49
  ],
30
50
  "compatibility": {
31
- "groma": "^0.3.0"
51
+ "groma": ">=0.5.0"
32
52
  }
33
53
  }
34
54
  }
package/validation.md CHANGED
@@ -9,7 +9,7 @@ project build were installed or run.
9
9
  manifest rule.
10
10
  - 47 authored files were inventoried with 122 declarations, 1,249 operations,
11
11
  758 of them carrying body tokens, and 4,762 calls, every one unresolved.
12
- - 579 files under `vendor/` stayed out through the shared Git boundary.
12
+ - 579 files under `vendor/` stayed out of the scan.
13
13
  - Repeating the scan produced identical evidence. The first fold created 49
14
14
  elements; folding the same evidence again created none and refreshed the same
15
15
  47 files.
@@ -22,16 +22,10 @@ project build were installed or run.
22
22
  no HTTP row was derived there.
23
23
  - Each scan of the whole project finished in under a second.
24
24
 
25
- The minified rules were measured against 137 authored candidate files in four
26
- JavaScript repositories. The longest-lined authored file averaged 57 characters
27
- per line. Exactly one file reached the 500-character average: a built asset under
28
- an ordinary `.js` name, which the rule excluded. Vendored and built directories
29
- are already outside the shared Git boundary.
30
-
31
25
  The independent `javascript-source` fixture verifies ECMAScript modules,
32
26
  CommonJS, JSX, a browser script whose top-level calls belong to the module,
33
27
  exact source positions, tokens on named bodies only, and the exclusion of a
34
- `.min.js` name, of a long-lined bundle, and of the TypeScript file beside them.
28
+ `.min.js` name by the default exclusions and of the TypeScript file beside it.
35
29
  `javascript-outline` verifies the declaration list and the module, CommonJS and
36
30
  script visibility rules, including a CommonJS file that publishes a function value
37
31
  and keeps its own helpers private; `javascript-parity` verifies that one body