@nodesecure/js-x-ray 7.1.1 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -223,6 +223,8 @@ interface RuntimeOptions {
223
223
  module?: boolean;
224
224
  removeHTMLComments?: boolean;
225
225
  isMinified?: boolean;
226
+ initialize?: (sourceFile: SourceFile) => void;
227
+ finalize?: (sourceFile: SourceFile) => void;
226
228
  }
227
229
  ```
228
230
 
@@ -256,6 +258,8 @@ interface RuntimeFileOptions {
256
258
  module?: boolean;
257
259
  removeHTMLComments?: boolean;
258
260
  packageName?: string;
261
+ initialize?: (sourceFile: SourceFile) => void;
262
+ finalize?: (sourceFile: SourceFile) => void;
259
263
  }
260
264
  ```
261
265
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/js-x-ray",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
4
4
  "description": "JavaScript AST XRay analysis",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -1,137 +1,160 @@
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
- }
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
+ initialize,
36
+ finalize
37
+ } = options;
38
+
39
+ const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
40
+ isEcmaScriptModule: Boolean(module)
41
+ });
42
+ const source = new SourceFile(str, this.probesOptions);
43
+
44
+ // TODO: this check should be factorized in a way that we reuse it
45
+ // on analyze and anlyseFile
46
+ if (initialize) {
47
+ if (typeof initialize !== "function") {
48
+ throw new TypeError("options.initialize must be a function");
49
+ }
50
+ initialize(source);
51
+ }
52
+
53
+ // we walk each AST Nodes, this is a purely synchronous I/O
54
+ walk(body, {
55
+ enter(node) {
56
+ // Skip the root of the AST.
57
+ if (Array.isArray(node)) {
58
+ return;
59
+ }
60
+
61
+ const action = source.walk(node);
62
+ if (action === "skip") {
63
+ this.skip();
64
+ }
65
+ }
66
+ });
67
+
68
+ // TODO: this check should be factorized in a way that we reuse it
69
+ // on analyze and anlyseFile
70
+ if (finalize) {
71
+ if (typeof finalize !== "function") {
72
+ throw new TypeError("options.finalize must be a function");
73
+ }
74
+ finalize(source);
75
+ }
76
+
77
+ return {
78
+ ...source.getResult(isMinified),
79
+ dependencies: source.dependencies,
80
+ isOneLineRequire: isOneLineExpressionExport(body)
81
+ };
82
+ }
83
+
84
+ async analyseFile(
85
+ pathToFile,
86
+ options = {}
87
+ ) {
88
+ try {
89
+ const {
90
+ packageName = null,
91
+ module = true,
92
+ removeHTMLComments = false,
93
+ initialize,
94
+ finalize
95
+ } = options;
96
+
97
+ const str = await fs.readFile(pathToFile, "utf-8");
98
+ const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
99
+
100
+ const isMin = filePathString.includes(".min") || isMinified(str);
101
+ const data = this.analyse(str, {
102
+ isMinified: isMin,
103
+ module: path.extname(filePathString) === ".mjs" ? true : module,
104
+ removeHTMLComments,
105
+ initialize,
106
+ finalize
107
+ });
108
+
109
+ if (packageName !== null) {
110
+ data.dependencies.delete(packageName);
111
+ }
112
+
113
+ return {
114
+ ok: true,
115
+ dependencies: data.dependencies,
116
+ warnings: data.warnings,
117
+ isMinified: !data.isOneLineRequire && isMin
118
+ };
119
+ }
120
+ catch (error) {
121
+ return {
122
+ ok: false,
123
+ warnings: [
124
+ { kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
125
+ ]
126
+ };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * @param {!string} source
132
+ * @param {object} options
133
+ * @param {boolean} [options.removeHTMLComments=false]
134
+ */
135
+ prepareSource(source, options = {}) {
136
+ if (typeof source !== "string") {
137
+ throw new TypeError("source must be a string");
138
+ }
139
+ const { removeHTMLComments = false } = options;
140
+
141
+ /**
142
+ * if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
143
+ * @example
144
+ * #!/usr/bin/env node
145
+ */
146
+ const rawNoShebang = source.startsWith("#") ?
147
+ source.slice(source.indexOf("\n") + 1) : source;
148
+
149
+ return removeHTMLComments ?
150
+ this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
151
+ }
152
+
153
+ /**
154
+ * @param {!string} str
155
+ * @returns {string}
156
+ */
157
+ #removeHTMLComment(str) {
158
+ return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
159
+ }
160
+ }
@@ -1,99 +1,112 @@
1
- // Import Node.js Dependencies
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
-
5
- // Import Internal Dependencies
6
- import { AstAnalyser } from "./AstAnalyser.js";
7
-
8
- const kDefaultExtensions = ["js", "cjs", "mjs", "node"];
9
-
10
- export class EntryFilesAnalyser {
11
- /**
12
- * @constructor
13
- * @param {object} [options={}]
14
- * @param {AstAnalyser} [options.astAnalyzer=new AstAnalyser()]
15
- * @param {function} [options.loadExtensions]
16
- */
17
- constructor(options = {}) {
18
- this.astAnalyzer = options.astAnalyzer ?? new AstAnalyser();
19
- this.allowedExtensions = options.loadExtensions
20
- ? options.loadExtensions(kDefaultExtensions)
21
- : kDefaultExtensions;
22
- }
23
-
24
- /**
25
- * Asynchronously analyze a set of entry files yielding analysis reports.
26
- *
27
- * @param {(string | URL)[]} entryFiles
28
- * @yields {Object} - Yields an object containing the analysis report for each file.
29
- */
30
- async* analyse(entryFiles) {
31
- this.analyzedDeps = new Set();
32
-
33
- for (const file of entryFiles) {
34
- yield* this.#analyzeFile(file);
35
- }
36
- }
37
-
38
- async* #analyzeFile(file) {
39
- const filePath = file instanceof URL ? file.pathname : file;
40
- const report = await this.astAnalyzer.analyseFile(file);
41
-
42
- yield { url: filePath, ...report };
43
-
44
- if (!report.ok) {
45
- return;
46
- }
47
-
48
- yield* this.#analyzeDeps(report.dependencies, path.dirname(filePath));
49
- }
50
-
51
- async* #analyzeDeps(deps, basePath) {
52
- for (const [name] of deps) {
53
- const depPath = await this.#getInternalDepPath(name, basePath);
54
- if (depPath && !this.analyzedDeps.has(depPath)) {
55
- this.analyzedDeps.add(depPath);
56
-
57
- yield* this.#analyzeFile(depPath);
58
- }
59
- }
60
- }
61
-
62
- async #getInternalDepPath(name, basePath) {
63
- const depPath = path.join(basePath, name);
64
- const existingExt = path.extname(name);
65
- if (existingExt !== "") {
66
- if (!this.allowedExtensions.includes(existingExt.slice(1))) {
67
- return null;
68
- }
69
-
70
- if (await this.#fileExists(depPath)) {
71
- return depPath;
72
- }
73
- }
74
-
75
- for (const ext of this.allowedExtensions) {
76
- const depPathWithExt = `${depPath}.${ext}`;
77
- if (await this.#fileExists(depPathWithExt)) {
78
- return depPathWithExt;
79
- }
80
- }
81
-
82
- return null;
83
- }
84
-
85
- async #fileExists(path) {
86
- try {
87
- await fs.access(path, fs.constants.F_OK);
88
-
89
- return true;
90
- }
91
- catch (error) {
92
- if (error.code !== "ENOENT") {
93
- throw error;
94
- }
95
-
96
- return false;
97
- }
98
- }
99
- }
1
+ // Import Node.js Dependencies
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // Import Internal Dependencies
7
+ import { AstAnalyser } from "./AstAnalyser.js";
8
+
9
+ // CONSTANTS
10
+ const kDefaultExtensions = ["js", "cjs", "mjs", "node"];
11
+
12
+ export class EntryFilesAnalyser {
13
+ /**
14
+ * @constructor
15
+ * @param {object} [options={}]
16
+ * @param {AstAnalyser} [options.astAnalyzer=new AstAnalyser()]
17
+ * @param {function} [options.loadExtensions]
18
+ */
19
+ constructor(options = {}) {
20
+ this.astAnalyzer = options.astAnalyzer ?? new AstAnalyser();
21
+ const rawAllowedExtensions = options.loadExtensions
22
+ ? options.loadExtensions(kDefaultExtensions)
23
+ : kDefaultExtensions;
24
+
25
+ this.allowedExtensions = new Set(rawAllowedExtensions);
26
+ }
27
+
28
+ /**
29
+ * Asynchronously analyze a set of entry files yielding analysis reports.
30
+ *
31
+ * @param {(string | URL)[]} entryFiles
32
+ * @yields {Object} - Yields an object containing the analysis report for each file.
33
+ */
34
+ async* analyse(entryFiles) {
35
+ this.analyzedDeps = new Set();
36
+
37
+ for (const file of entryFiles) {
38
+ yield* this.#analyzeFile(file);
39
+ }
40
+ }
41
+
42
+ async* #analyzeFile(file) {
43
+ const filePath = file instanceof URL ? fileURLToPath(file) : file;
44
+ const report = await this.astAnalyzer.analyseFile(file);
45
+
46
+ yield { url: filePath, ...report };
47
+
48
+ if (!report.ok) {
49
+ return;
50
+ }
51
+
52
+ yield* this.#analyzeDeps(
53
+ report.dependencies,
54
+ path.dirname(filePath)
55
+ );
56
+ }
57
+
58
+ async* #analyzeDeps(deps, basePath) {
59
+ for (const [name] of deps) {
60
+ const depPath = await this.#getInternalDepPath(name, basePath);
61
+
62
+ if (depPath && !this.analyzedDeps.has(depPath)) {
63
+ this.analyzedDeps.add(depPath);
64
+
65
+ yield* this.#analyzeFile(depPath);
66
+ }
67
+ }
68
+ }
69
+
70
+ async #getInternalDepPath(name, basePath) {
71
+ const depPath = path.join(basePath, name);
72
+ const existingExt = path.extname(name);
73
+
74
+ if (existingExt === "") {
75
+ for (const ext of this.allowedExtensions) {
76
+ const depPathWithExt = `${depPath}.${ext}`;
77
+
78
+ const fileExist = await this.#fileExists(depPathWithExt);
79
+ if (fileExist) {
80
+ return depPathWithExt;
81
+ }
82
+ }
83
+ }
84
+ else {
85
+ if (!this.allowedExtensions.has(existingExt.slice(1))) {
86
+ return null;
87
+ }
88
+
89
+ const fileExist = await this.#fileExists(depPath);
90
+ if (fileExist) {
91
+ return depPath;
92
+ }
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ async #fileExists(path) {
99
+ try {
100
+ await fs.access(path, fs.constants.R_OK);
101
+
102
+ return true;
103
+ }
104
+ catch (error) {
105
+ if (error.code !== "ENOENT") {
106
+ throw error;
107
+ }
108
+
109
+ return false;
110
+ }
111
+ }
112
+ }
package/types/api.d.ts CHANGED
@@ -53,6 +53,8 @@ interface RuntimeOptions {
53
53
  * @default false
54
54
  */
55
55
  isMinified?: boolean;
56
+ initialize?: (sourceFile: SourceFile) => void;
57
+ finalize?: (sourceFile: SourceFile) => void;
56
58
  }
57
59
 
58
60
  interface RuntimeFileOptions extends Omit<RuntimeOptions, "isMinified"> {