@nodesecure/js-x-ray 6.3.0 → 7.1.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.
Files changed (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -18
  3. package/index.d.ts +16 -3
  4. package/index.js +37 -134
  5. package/package.json +9 -9
  6. package/src/AstAnalyser.js +137 -0
  7. package/src/Deobfuscator.js +192 -0
  8. package/src/EntryFilesAnalyser.js +99 -0
  9. package/src/JsSourceParser.js +57 -0
  10. package/src/NodeCounter.js +76 -0
  11. package/src/ProbeRunner.js +140 -0
  12. package/src/{Analysis.js → SourceFile.js} +55 -68
  13. package/src/obfuscators/freejsobfuscator.js +9 -9
  14. package/src/obfuscators/jjencode.js +7 -7
  15. package/src/obfuscators/jsfuck.js +6 -6
  16. package/src/obfuscators/obfuscator-io.js +7 -7
  17. package/src/obfuscators/trojan-source.js +28 -28
  18. package/src/probes/isArrayExpression.js +32 -30
  19. package/src/probes/isBinaryExpression.js +5 -3
  20. package/src/probes/isImportDeclaration.js +8 -5
  21. package/src/probes/isLiteral.js +17 -9
  22. package/src/probes/isLiteralRegex.js +5 -3
  23. package/src/probes/isRegexObject.js +5 -3
  24. package/src/probes/isRequire/RequireCallExpressionWalker.js +93 -0
  25. package/src/probes/isRequire/isRequire.js +142 -0
  26. package/src/probes/isUnsafeCallee.js +15 -5
  27. package/src/probes/isWeakCrypto.js +5 -3
  28. package/src/utils/exportAssignmentHasRequireLeave.js +40 -0
  29. package/src/utils/extractNode.js +14 -0
  30. package/src/utils/index.js +8 -0
  31. package/src/utils/isNode.js +5 -0
  32. package/src/utils/isOneLineExpressionExport.js +18 -0
  33. package/src/utils/isUnsafeCallee.js +28 -0
  34. package/src/utils/notNullOrUndefined.js +3 -0
  35. package/src/utils/rootLocation.js +3 -0
  36. package/src/utils/toArrayLocation.js +11 -0
  37. package/src/warnings.js +1 -1
  38. package/types/api.d.ts +79 -18
  39. package/src/ASTDeps.js +0 -63
  40. package/src/obfuscators/index.js +0 -69
  41. package/src/probes/index.js +0 -70
  42. package/src/probes/isAssignmentExpression.js +0 -29
  43. package/src/probes/isClassDeclaration.js +0 -25
  44. package/src/probes/isFunction.js +0 -38
  45. package/src/probes/isMemberExpression.js +0 -16
  46. package/src/probes/isMethodDefinition.js +0 -25
  47. package/src/probes/isObjectExpression.js +0 -29
  48. package/src/probes/isRequire.js +0 -164
  49. package/src/probes/isUnaryExpression.js +0 -26
  50. package/src/probes/isVariableDeclaration.js +0 -30
  51. package/src/utils.js +0 -48
  52. package/types/astdeps.d.ts +0 -34
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 NodeSecure
3
+ Copyright (c) 2021-2024 NodeSecure
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -68,22 +68,19 @@ require(Buffer.from("6673", "hex").toString());
68
68
  Then use `js-x-ray` to run an analysis of the JavaScript code:
69
69
  ```js
70
70
  import { runASTAnalysis } from "@nodesecure/js-x-ray";
71
- import { readFileSync } from "fs";
71
+ import { readFileSync } from "node:fs";
72
72
 
73
- const str = readFileSync("./file.js", "utf-8");
74
- const { warnings, dependencies } = runASTAnalysis(str);
73
+ const { warnings, dependencies } = runASTAnalysis(
74
+ readFileSync("./file.js", "utf-8")
75
+ );
75
76
 
76
- const dependenciesName = [...dependencies];
77
- const inTryDeps = [...dependencies.getDependenciesInTryStatement()];
78
-
79
- console.log(dependenciesName);
80
- console.log(inTryDeps);
81
- console.log(warnings);
77
+ console.log(dependencies);
78
+ console.dir(warnings, { depth: null });
82
79
  ```
83
80
 
84
81
  The analysis will return: `http` (in try), `crypto`, `util` and `fs`.
85
82
 
86
- > [!NOTE]
83
+ > [!TIP]
87
84
  > There is also a lot of suspicious code example in the `./examples` cases directory. Feel free to try the tool on these files.
88
85
 
89
86
  ## Warnings
@@ -137,16 +134,90 @@ This section describe all the possible warnings returned by JSXRay. Click on the
137
134
  | [weak-crypto](./docs/weak-crypto.md) | ✔️ | The code probably contains a weak crypto algorithm (md5, sha1...) |
138
135
  | [shady-link](./docs/shady-link.md) | ✔️ | The code contains shady/unsafe link |
139
136
 
140
- ## API
137
+ ## Custom Probes
138
+
139
+ You can also create custom probes to detect specific pattern in the code you are analyzing.
140
+
141
+ A probe is a pair of two functions (`validateNode` and `main`) that will be called on each node of the AST. It will return a warning if the pattern is detected.
142
+ Below a basic probe that detect a string assignation to `danger`:
143
+
144
+ ```ts
145
+ export const customProbes = [
146
+ {
147
+ name: "customProbeUnsafeDanger",
148
+ validateNode: (node, sourceFile) => [
149
+ node.type === "VariableDeclaration" && node.declarations[0].init.value === "danger"
150
+ ],
151
+ main: (node, options) => {
152
+ const { sourceFile, data: calleeName } = options;
153
+ if (node.declarations[0].init.value === "danger") {
154
+ sourceFile.addWarning("unsafe-danger", calleeName, node.loc);
155
+
156
+ return ProbeSignals.Skip;
157
+ }
158
+
159
+ return null;
160
+ }
161
+ }
162
+ ];
163
+ ```
164
+
165
+ You can pass an array of probes to the `runASTAnalysis/runASTAnalysisOnFile` functions as `options`, or directly to the `AstAnalyser` constructor.
166
+
167
+ | Name | Type | Description | Default Value |
168
+ |------------------|----------------------------------|-----------------------------------------------------------------------|-----------------|
169
+ | `customParser` | `SourceParser \| undefined` | An optional custom parser to be used for parsing the source code. | `JsSourceParser` |
170
+ | `customProbes` | `Probe[] \| undefined` | An array of custom probes to be used during AST analysis. | `[]` |
171
+ | `skipDefaultProbes` | `boolean \| undefined` | If `true`, default probes will be skipped and only custom probes will be used. | `false` |
172
+
141
173
 
174
+ Here using the example probe upper:
175
+
176
+ ```ts
177
+ import { runASTAnalysis } from "@nodesecure/js-x-ray";
178
+
179
+ // add your customProbes here (see example above)
180
+
181
+ const result = runASTAnalysis("const danger = 'danger';", { customProbes, skipDefaultProbes: true });
182
+
183
+ console.log(result);
184
+ ```
185
+
186
+ Result:
187
+
188
+ ```sh
189
+ ✗ node example.js
190
+ {
191
+ idsLengthAvg: 0,
192
+ stringScore: 0,
193
+ warnings: [ { kind: 'unsafe-danger', location: [Array], source: 'JS-X-Ray' } ],
194
+ dependencies: Map(0) {},
195
+ isOneLineRequire: false
196
+ }
197
+ ```
198
+
199
+ Congrats, you have created your first custom probe! 🎉
200
+
201
+ > [!TIP]
202
+ > Check the types in [index.d.ts](index.d.ts) and [types/api.d.ts](types/api.d.ts) for more details about the `options`
203
+
204
+ ## API
142
205
  <details>
143
- <summary>runASTAnalysis(str: string, options?: RuntimeOptions): Report</summary>
206
+ <summary>runASTAnalysis(str: string, options?: RuntimeOptions & AstAnalyserOptions): Report</summary>
144
207
 
145
208
  ```ts
146
209
  interface RuntimeOptions {
147
210
  module?: boolean;
148
- isMinified?: boolean;
149
211
  removeHTMLComments?: boolean;
212
+ isMinified?: boolean;
213
+ }
214
+ ```
215
+
216
+ ```ts
217
+ interface AstAnalyserOptions {
218
+ customParser?: SourceParser;
219
+ customProbes?: Probe[];
220
+ skipDefaultProbes?: boolean;
150
221
  }
151
222
  ```
152
223
 
@@ -165,13 +236,21 @@ interface Report {
165
236
  </details>
166
237
 
167
238
  <details>
168
- <summary>runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions): Promise< ReportOnFile ></summary>
239
+ <summary>runASTAnalysisOnFile(pathToFile: string, options?: RuntimeFileOptions & AstAnalyserOptions): Promise< ReportOnFile ></summary>
169
240
 
170
241
  ```ts
171
- interface RuntimeOptions {
242
+ interface RuntimeFileOptions {
172
243
  module?: boolean;
173
- isMinified?: boolean;
174
244
  removeHTMLComments?: boolean;
245
+ packageName?: string;
246
+ }
247
+ ```
248
+
249
+ ```ts
250
+ interface AstAnalyserOptions {
251
+ customParser?: SourceParser;
252
+ customProbes?: Probe[];
253
+ skipDefaultProbes?: boolean;
175
254
  }
176
255
  ```
177
256
 
@@ -197,8 +276,9 @@ Click on one of the links to access the documentation of the workspace:
197
276
 
198
277
  | name | package and link |
199
278
  | --- | --- |
200
- | estree-ast-util | [@nodesecure/estree-ast-util](./workspaces/estree-ast-util) |
279
+ | estree-ast-utils | [@nodesecure/estree-ast-utils](./workspaces/estree-ast-utils) |
201
280
  | sec-literal | [@nodesecure/sec-literal ](./workspaces/sec-literal) |
281
+ | ts-source-parser | [@nodesecure/ts-source-parser ](./workspaces/ts-source-parser) |
202
282
 
203
283
  These packages are available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
204
284
  ```bash
@@ -210,7 +290,7 @@ $ yarn add @nodesecure/estree-ast-util
210
290
  ## Contributors ✨
211
291
 
212
292
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
213
- [![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)
293
+ [![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors-)
214
294
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
215
295
 
216
296
  Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -234,6 +314,14 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
234
314
  <td align="center" valign="top" width="14.28%"><a href="https://maji.kiwi"><img src="https://avatars.githubusercontent.com/u/33150916?v=4?s=100" width="100px;" alt="Maji"/><br /><sub><b>Maji</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=M4gie" title="Code">💻</a></td>
235
315
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/targos"><img src="https://avatars.githubusercontent.com/u/2352663?v=4?s=100" width="100px;" alt="Michaël Zasso"/><br /><sub><b>Michaël Zasso</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=targos" title="Code">💻</a> <a href="https://github.com/NodeSecure/js-x-ray/issues?q=author%3Atargos" title="Bug reports">🐛</a></td>
236
316
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fabnguess"><img src="https://avatars.githubusercontent.com/u/72697416?v=4?s=100" width="100px;" alt="Kouadio Fabrice Nguessan"/><br /><sub><b>Kouadio Fabrice Nguessan</b></sub></a><br /><a href="#maintenance-fabnguess" title="Maintenance">🚧</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=fabnguess" title="Code">💻</a></td>
317
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/jean-michelet"><img src="https://avatars.githubusercontent.com/u/110341611?v=4?s=100" width="100px;" alt="Jean"/><br /><sub><b>Jean</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=jean-michelet" title="Tests">⚠️</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=jean-michelet" title="Code">💻</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=jean-michelet" title="Documentation">📖</a></td>
318
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/tchapacan"><img src="https://avatars.githubusercontent.com/u/28821702?v=4?s=100" width="100px;" alt="tchapacan"/><br /><sub><b>tchapacan</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=tchapacan" title="Code">💻</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=tchapacan" title="Tests">⚠️</a></td>
319
+ <td align="center" valign="top" width="14.28%"><a href="http://miikkak.dev"><img src="https://avatars.githubusercontent.com/u/65869801?v=4?s=100" width="100px;" alt="mkarkkainen"/><br /><sub><b>mkarkkainen</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=mkarkkainen" title="Code">💻</a></td>
320
+ </tr>
321
+ <tr>
322
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/FredGuiou"><img src="https://avatars.githubusercontent.com/u/99122562?v=4?s=100" width="100px;" alt="FredGuiou"/><br /><sub><b>FredGuiou</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=FredGuiou" title="Documentation">📖</a> <a href="https://github.com/NodeSecure/js-x-ray/commits?author=FredGuiou" title="Code">💻</a></td>
323
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/madina0801"><img src="https://avatars.githubusercontent.com/u/101329759?v=4?s=100" width="100px;" alt="Madina"/><br /><sub><b>Madina</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=madina0801" title="Code">💻</a></td>
324
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/sairuss7"><img src="https://avatars.githubusercontent.com/u/87803528?v=4?s=100" width="100px;" alt="SairussDev"/><br /><sub><b>SairussDev</b></sub></a><br /><a href="https://github.com/NodeSecure/js-x-ray/commits?author=sairuss7" title="Code">💻</a></td>
237
325
  </tr>
238
326
  </tbody>
239
327
  </table>
package/index.d.ts CHANGED
@@ -1,10 +1,19 @@
1
1
  import {
2
+ AstAnalyser,
3
+ AstAnalyserOptions,
4
+
5
+ EntryFilesAnalyser,
6
+ EntryFilesAnalyserOptions,
7
+
8
+ SourceParser,
2
9
  runASTAnalysis,
3
10
  runASTAnalysisOnFile,
4
11
  Report,
5
12
  ReportOnFile,
6
13
  RuntimeFileOptions,
7
- RuntimeOptions
14
+ RuntimeOptions,
15
+ SourceLocation,
16
+ Dependency
8
17
  } from "./types/api.js";
9
18
  import {
10
19
  Warning,
@@ -13,19 +22,23 @@ import {
13
22
  WarningName,
14
23
  WarningNameWithValue
15
24
  } from "./types/warnings.js";
16
- import { ASTDeps, Dependency } from "./types/astdeps.js";
17
25
 
18
26
  declare const warnings: Record<WarningName, Pick<WarningDefault, "experimental" | "i18n" | "severity">>;
19
27
 
20
28
  export {
21
29
  warnings,
30
+ AstAnalyser,
31
+ AstAnalyserOptions,
32
+ EntryFilesAnalyser,
33
+ EntryFilesAnalyserOptions,
34
+ SourceParser,
22
35
  runASTAnalysis,
23
36
  runASTAnalysisOnFile,
24
37
  Report,
25
38
  ReportOnFile,
26
39
  RuntimeFileOptions,
27
40
  RuntimeOptions,
28
- ASTDeps,
41
+ SourceLocation,
29
42
  Dependency,
30
43
  Warning,
31
44
  WarningDefault,
package/index.js CHANGED
@@ -1,148 +1,51 @@
1
- // Import Node.js Dependencies
2
- import fs from "fs/promises";
3
- import path from "path";
4
-
5
- // Import Third-party Dependencies
6
- import { walk } from "estree-walker";
7
- import * as meriyah from "meriyah";
8
- import isMinified from "is-minified-code";
9
-
10
1
  // Import Internal Dependencies
11
- import Analysis from "./src/Analysis.js";
12
2
  import { warnings } from "./src/warnings.js";
13
- import * as utils from "./src/utils.js";
14
-
15
- // CONSTANTS
16
- const kMeriyahDefaultOptions = {
17
- next: true,
18
- loc: true,
19
- raw: true,
20
- jsx: true
21
- };
3
+ import { JsSourceParser } from "./src/JsSourceParser.js";
4
+ import { AstAnalyser } from "./src/AstAnalyser.js";
22
5
 
23
- export function runASTAnalysis(str, options = Object.create(null)) {
6
+ function runASTAnalysis(
7
+ str,
8
+ options = Object.create(null)
9
+ ) {
24
10
  const {
25
- module = true,
26
- isMinified = false,
27
- removeHTMLComments = false
11
+ customParser = new JsSourceParser(),
12
+ customProbes = [],
13
+ skipDefaultProbes = false,
14
+ ...opts
28
15
  } = options;
29
16
 
30
- // Note: if the file start with a shebang then we remove it because 'parseScript' may fail to parse it.
31
- // Example: #!/usr/bin/env node
32
- const strToAnalyze = str.charAt(0) === "#" ? str.slice(str.indexOf("\n")) : str;
33
- const body = parseScriptExtended(strToAnalyze, {
34
- isEcmaScriptModule: Boolean(module),
35
- removeHTMLComments
17
+ const analyser = new AstAnalyser({
18
+ customParser,
19
+ customProbes,
20
+ skipDefaultProbes
36
21
  });
37
22
 
38
- const sastAnalysis = new Analysis();
39
- sastAnalysis.analyzeSourceString(str);
40
-
41
- // we walk each AST Nodes, this is a purely synchronous I/O
42
- walk(body, {
43
- enter(node) {
44
- // Skip the root of the AST.
45
- if (Array.isArray(node)) {
46
- return;
47
- }
48
-
49
- const action = sastAnalysis.walk(node);
50
- if (action === "skip") {
51
- this.skip();
52
- }
53
- }
54
- });
55
-
56
- const dependencies = sastAnalysis.dependencies;
57
- const { idsLengthAvg, stringScore, warnings } = sastAnalysis.getResult(isMinified);
58
- const isOneLineRequire = body.length <= 1 && dependencies.size <= 1;
59
-
60
- return {
61
- dependencies, warnings, idsLengthAvg, stringScore, isOneLineRequire
62
- };
63
- }
64
-
65
- export async function runASTAnalysisOnFile(pathToFile, options = {}) {
66
- try {
67
- const {
68
- packageName = null,
69
- module = true,
70
- removeHTMLComments = false
71
- } = options;
72
-
73
- const str = await fs.readFile(pathToFile, "utf-8");
74
- const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
75
-
76
- const isMin = filePathString.includes(".min") || isMinified(str);
77
- const data = runASTAnalysis(str, {
78
- isMinified: isMin,
79
- module: path.extname(filePathString) === ".mjs" ? true : module,
80
- removeHTMLComments
81
- });
82
- if (packageName !== null) {
83
- data.dependencies.removeByName(packageName);
84
- }
85
-
86
- return {
87
- ok: true,
88
- dependencies: data.dependencies,
89
- warnings: data.warnings,
90
- isMinified: !data.isOneLineRequire && isMin
91
- };
92
- }
93
- catch (error) {
94
- return {
95
- ok: false,
96
- warnings: [
97
- { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
98
- ]
99
- };
100
- }
23
+ return analyser.analyse(str, opts);
101
24
  }
102
25
 
103
- function parseScriptExtended(strToAnalyze, options = {}) {
104
- const { isEcmaScriptModule, removeHTMLComments } = options;
105
-
106
- /**
107
- * @see https://github.com/NodeSecure/js-x-ray/issues/109
108
- */
109
- const cleanedStrToAnalyze = removeHTMLComments ?
110
- utils.removeHTMLComment(strToAnalyze) : strToAnalyze;
111
-
112
- try {
113
- const { body } = meriyah.parseScript(
114
- cleanedStrToAnalyze,
115
- {
116
- ...kMeriyahDefaultOptions,
117
- module: isEcmaScriptModule,
118
- globalReturn: !isEcmaScriptModule
119
- }
120
- );
121
-
122
- return body;
123
- }
124
- catch (error) {
125
- const isIllegalReturn = error.description.includes("Illegal return statement");
126
-
127
- if (error.name === "SyntaxError" && (
128
- error.description.includes("The import keyword") ||
129
- error.description.includes("The export keyword") ||
130
- isIllegalReturn
131
- )) {
132
- const { body } = meriyah.parseScript(
133
- cleanedStrToAnalyze,
134
- {
135
- ...kMeriyahDefaultOptions,
136
- module: true,
137
- globalReturn: isIllegalReturn
138
- }
139
- );
26
+ async function runASTAnalysisOnFile(
27
+ pathToFile,
28
+ options = {}
29
+ ) {
30
+ const {
31
+ customParser = new JsSourceParser(),
32
+ customProbes = [],
33
+ skipDefaultProbes = false,
34
+ ...opts
35
+ } = options;
140
36
 
141
- return body;
142
- }
37
+ const analyser = new AstAnalyser({
38
+ customParser,
39
+ customProbes,
40
+ skipDefaultProbes
41
+ });
143
42
 
144
- throw error;
145
- }
43
+ return analyser.analyseFile(pathToFile, opts);
146
44
  }
147
45
 
148
- export { warnings };
46
+ export {
47
+ warnings,
48
+ AstAnalyser,
49
+ runASTAnalysis,
50
+ runASTAnalysisOnFile
51
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/js-x-ray",
3
- "version": "6.3.0",
3
+ "version": "7.1.0",
4
4
  "description": "JavaScript AST XRay analysis",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "workspaces": [
22
22
  "workspaces/estree-ast-utils",
23
- "workspaces/sec-literal"
23
+ "workspaces/sec-literal",
24
+ "workspaces/ts-source-parser"
24
25
  ],
25
26
  "keywords": [
26
27
  "ast",
@@ -46,21 +47,20 @@
46
47
  "@nodesecure/estree-ast-utils": "^1.3.1",
47
48
  "@nodesecure/sec-literal": "^1.2.0",
48
49
  "estree-walker": "^3.0.1",
50
+ "frequency-set": "^1.0.2",
49
51
  "is-minified-code": "^2.0.0",
50
52
  "meriyah": "^4.3.3",
51
- "safe-regex": "^2.1.1"
53
+ "safe-regex": "^2.1.1",
54
+ "ts-pattern": "^5.0.6"
52
55
  },
53
56
  "devDependencies": {
54
57
  "@nodesecure/eslint-config": "^1.6.0",
55
- "@small-tech/esm-tape-runner": "^2.0.0",
56
- "@small-tech/tap-monkey": "^1.4.0",
57
58
  "@types/node": "^20.6.2",
58
- "c8": "^8.0.1",
59
+ "c8": "^9.0.0",
59
60
  "cross-env": "^7.0.3",
60
- "eslint": "^8.31.0",
61
+ "eslint": "^9.0.0",
61
62
  "glob": "^10.3.4",
62
63
  "iterator-matcher": "^2.1.0",
63
- "pkg-ok": "^3.0.0",
64
- "tape": "^5.7.2"
64
+ "pkg-ok": "^3.0.0"
65
65
  }
66
66
  }
@@ -0,0 +1,137 @@
1
+ // Import Node.js Dependencies
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ // Import Third-party Dependencies
6
+ import { walk } from "estree-walker";
7
+ import isMinified from "is-minified-code";
8
+
9
+ // Import Internal Dependencies
10
+ import { SourceFile } from "./SourceFile.js";
11
+ import { isOneLineExpressionExport } from "./utils/index.js";
12
+ import { JsSourceParser } from "./JsSourceParser.js";
13
+
14
+ export class AstAnalyser {
15
+ /**
16
+ * @constructor
17
+ * @param {object} [options={}]
18
+ * @param {SourceParser} [options.customParser]
19
+ * @param {Array<object>} [options.customProbes]
20
+ * @param {boolean} [options.skipDefaultProbes=false]
21
+ */
22
+ constructor(options = {}) {
23
+ this.parser = options.customParser ?? new JsSourceParser();
24
+ this.probesOptions = {
25
+ customProbes: options.customProbes ?? [],
26
+ skipDefaultProbes: options.skipDefaultProbes ?? false
27
+ };
28
+ }
29
+
30
+ analyse(str, options = Object.create(null)) {
31
+ const {
32
+ isMinified = false,
33
+ module = true,
34
+ removeHTMLComments = false
35
+ } = options;
36
+
37
+ const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
38
+ isEcmaScriptModule: Boolean(module)
39
+ });
40
+
41
+ const source = new SourceFile(str, this.probesOptions);
42
+
43
+ // we walk each AST Nodes, this is a purely synchronous I/O
44
+ walk(body, {
45
+ enter(node) {
46
+ // Skip the root of the AST.
47
+ if (Array.isArray(node)) {
48
+ return;
49
+ }
50
+
51
+ const action = source.walk(node);
52
+ if (action === "skip") {
53
+ this.skip();
54
+ }
55
+ }
56
+ });
57
+
58
+ return {
59
+ ...source.getResult(isMinified),
60
+ dependencies: source.dependencies,
61
+ isOneLineRequire: isOneLineExpressionExport(body)
62
+ };
63
+ }
64
+
65
+ async analyseFile(
66
+ pathToFile,
67
+ options = {}
68
+ ) {
69
+ try {
70
+ const {
71
+ packageName = null,
72
+ module = true,
73
+ removeHTMLComments = false
74
+ } = options;
75
+
76
+ const str = await fs.readFile(pathToFile, "utf-8");
77
+ const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
78
+
79
+ const isMin = filePathString.includes(".min") || isMinified(str);
80
+ const data = this.analyse(str, {
81
+ isMinified: isMin,
82
+ module: path.extname(filePathString) === ".mjs" ? true : module,
83
+ removeHTMLComments
84
+ });
85
+
86
+ if (packageName !== null) {
87
+ data.dependencies.delete(packageName);
88
+ }
89
+
90
+ return {
91
+ ok: true,
92
+ dependencies: data.dependencies,
93
+ warnings: data.warnings,
94
+ isMinified: !data.isOneLineRequire && isMin
95
+ };
96
+ }
97
+ catch (error) {
98
+ return {
99
+ ok: false,
100
+ warnings: [
101
+ { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
102
+ ]
103
+ };
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @param {!string} source
109
+ * @param {object} options
110
+ * @param {boolean} [options.removeHTMLComments=false]
111
+ */
112
+ prepareSource(source, options = {}) {
113
+ if (typeof source !== "string") {
114
+ throw new TypeError("source must be a string");
115
+ }
116
+ const { removeHTMLComments = false } = options;
117
+
118
+ /**
119
+ * if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
120
+ * @example
121
+ * #!/usr/bin/env node
122
+ */
123
+ const rawNoShebang = source.startsWith("#") ?
124
+ source.slice(source.indexOf("\n") + 1) : source;
125
+
126
+ return removeHTMLComments ?
127
+ this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
128
+ }
129
+
130
+ /**
131
+ * @param {!string} str
132
+ * @returns {string}
133
+ */
134
+ #removeHTMLComment(str) {
135
+ return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
136
+ }
137
+ }