@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 +52 -37
- package/dist/index.js +514 -175
- package/package.json +22 -2
- package/validation.md +2 -8
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# JavaScript scanner
|
|
2
2
|
|
|
3
|
-
The JavaScript scanner reads
|
|
4
|
-
|
|
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
|
|
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
|
-
##
|
|
34
|
+
## Included and excluded files
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
source
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 `*`
|
|
213
|
-
|
|
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.
|
|
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
|
|
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
|
|
248
|
-
`app.use('/static', express.static('public'))`, a router the
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
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
|
|
1600
|
-
for (const call of calls) {
|
|
1677
|
+
const candidates = calls.flatMap((call) => {
|
|
1601
1678
|
const member = memberOf(context.ts, call);
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1820
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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
|
|
2229
|
+
const [first, second] = call.arguments;
|
|
2100
2230
|
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
|
|
2101
|
-
|
|
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([...
|
|
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
|
|
2357
|
-
|
|
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
|
|
2363
|
-
|
|
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) =>
|
|
2367
|
-
if (exports.length > 0 || MODULE_EXTENSION.test(file) ||
|
|
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
|
|
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
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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
|
|
2416
|
-
|
|
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
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
|
2427
|
-
|
|
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
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
-
|
|
2435
|
-
const
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
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:
|
|
2489
|
-
roots: [{ id: "javascript-source", kind: "source-group", name:
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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
|