Skip to content

Commit 52a0ac5

Browse files
committed
refactor: upgrade to js-x-ray 13
1 parent 45eb26a commit 52a0ac5

15 files changed

Lines changed: 977 additions & 389 deletions

.changeset/orange-ants-unite.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@nodesecure/tree-walker": minor
3+
"@nodesecure/scanner": minor
4+
"@nodesecure/tarball": minor
5+
"@nodesecure/rc": minor
6+
---
7+
8+
refactor: upgrade to js-x-ray 13

workspaces/rc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"ajv": "6.12.6"
4646
},
4747
"dependencies": {
48-
"@nodesecure/js-x-ray": "12.0.0",
48+
"@nodesecure/js-x-ray": "13.0.0",
4949
"@nodesecure/npm-types": "^1.2.0",
5050
"@nodesecure/vulnera": "^2.0.1",
5151
"@openally/config": "^1.0.1",

workspaces/scanner/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"@nodesecure/contact": "^3.0.0",
6969
"@nodesecure/flags": "^3.0.3",
7070
"@nodesecure/i18n": "^4.1.0",
71-
"@nodesecure/js-x-ray": "12.0.0",
71+
"@nodesecure/js-x-ray": "13.0.0",
7272
"@nodesecure/mama": "^2.1.1",
7373
"@nodesecure/npm-registry-sdk": "^4.4.0",
7474
"@nodesecure/npm-types": "^1.3.0",

workspaces/tarball/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"dependencies": {
4848
"@nodesecure/conformance": "^1.2.1",
4949
"@nodesecure/fs-walk": "^2.0.0",
50-
"@nodesecure/js-x-ray": "12.0.0",
50+
"@nodesecure/js-x-ray": "13.0.0",
5151
"@nodesecure/mama": "^2.1.1",
5252
"@nodesecure/npm-types": "^1.2.0",
5353
"@nodesecure/utils": "^2.3.0",
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
// Import Node.js Dependencies
2+
import path from "node:path";
3+
4+
// Import Third-party Dependencies
5+
import { ManifestManager, parseNpmSpec } from "@nodesecure/mama";
6+
import {
7+
type Dependency,
8+
type CollectableSet,
9+
type CollectableInfos
10+
} from "@nodesecure/js-x-ray";
11+
import type { NodeImport } from "@nodesecure/npm-types";
12+
13+
export const NODE_BUILTINS = new Set([
14+
"assert",
15+
"assert/strict",
16+
"buffer",
17+
"child_process",
18+
"cluster",
19+
"console",
20+
"constants",
21+
"crypto",
22+
"dgram",
23+
"dns",
24+
"dns/promises",
25+
"domain",
26+
"events",
27+
"fs",
28+
"fs/promises",
29+
"http",
30+
"https",
31+
"module",
32+
"net",
33+
"os",
34+
"smalloc",
35+
"path",
36+
"path/posix",
37+
"path/win32",
38+
"punycode",
39+
"querystring",
40+
"readline",
41+
"readline/promises",
42+
"repl",
43+
"stream",
44+
"stream/web",
45+
"stream/promises",
46+
"stream/consumers",
47+
"_stream_duplex",
48+
"_stream_passthrough",
49+
"_stream_readable",
50+
"_stream_transform",
51+
"_stream_writable",
52+
"_stream_wrap",
53+
"string_decoder",
54+
"sys",
55+
"timers",
56+
"timers/promises",
57+
"tls",
58+
"tty",
59+
"url",
60+
"util",
61+
"util/types",
62+
"vm",
63+
"zlib",
64+
"freelist",
65+
"v8",
66+
"v8/tools/arguments",
67+
"v8/tools/codemap",
68+
"v8/tools/consarray",
69+
"v8/tools/csvparser",
70+
"v8/tools/logreader",
71+
"v8/tools/profile_view",
72+
"v8/tools/splaytree",
73+
"process",
74+
"inspector",
75+
"inspector/promises",
76+
"async_hooks",
77+
"http2",
78+
"perf_hooks",
79+
"trace_events",
80+
"worker_threads",
81+
"node:test",
82+
"test/reporters",
83+
"test/mock_loader",
84+
"node:sea",
85+
"node:sqlite",
86+
"wasi",
87+
"diagnostics_channel"
88+
]);
89+
90+
const kFileExtensions = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".node", ".json"];
91+
const kExternalModules = new Set(["http", "https", "net", "http2", "dgram", "child_process"]);
92+
const kExternalThirdPartyDeps = new Set([
93+
"undici",
94+
"node-fetch",
95+
"execa",
96+
"cross-spawn",
97+
"got",
98+
"axios",
99+
"ky",
100+
"superagent",
101+
"cross-fetch"
102+
]);
103+
const kRelativeImportPath = new Set([".", "..", "./", "../"]);
104+
105+
type Metadata = Dependency & { relativeFile: string; };
106+
107+
export class DependencyCollectableSet implements CollectableSet<Metadata> {
108+
type = "dependency";
109+
dependencies: Record<
110+
string,
111+
Record<string, Dependency>
112+
> = Object.create(null);
113+
#values: Set<string> = new Set();
114+
#files: Set<string> = new Set();
115+
#dependenciesInTryBlock: Set<string> = new Set();
116+
#subpathImportsDependencies: Record<string, string> = {};
117+
#thirdPartyDependencies: Set<string> = new Set();
118+
#thirdPartyAliasedDependencies: Set<string> = new Set();
119+
#missingDependencies: Set<string> = new Set();
120+
#nodeDependencies: Set<string> = new Set();
121+
#mama: Pick<ManifestManager, "dependencies" | "devDependencies" | "nodejsImports">;
122+
#hasExternalCapacity: boolean = false;
123+
124+
constructor(mama: Pick<ManifestManager, "dependencies" | "devDependencies" | "nodejsImports">) {
125+
this.#mama = mama;
126+
}
127+
128+
extract() {
129+
const unusedDependencies = this.#difference(
130+
this.#mama.dependencies.filter((name) => !name.startsWith("@types")),
131+
[...this.#thirdPartyDependencies, ...this.#thirdPartyAliasedDependencies]
132+
);
133+
const hasMissingOrUnusedDependency =
134+
unusedDependencies.length > 0 ||
135+
this.#missingDependencies.size > 0;
136+
137+
return {
138+
files: this.#files,
139+
dependenciesInTryBlock: [...this.#dependenciesInTryBlock],
140+
dependencies: {
141+
nodeJs: [...this.#nodeDependencies],
142+
thirdparty: [...this.#thirdPartyDependencies],
143+
subpathImports: this.#subpathImportsDependencies,
144+
unused: unusedDependencies,
145+
missing: [...this.#missingDependencies]
146+
},
147+
flags: {
148+
hasExternalCapacity: this.#hasExternalCapacity,
149+
hasMissingOrUnusedDependency
150+
}
151+
};
152+
}
153+
154+
add(value: string, { metadata }: CollectableInfos<Metadata>) {
155+
const relativeFile = metadata?.relativeFile!;
156+
if (!(relativeFile in this.dependencies)) {
157+
this.dependencies[relativeFile] = Object.create(null);
158+
}
159+
160+
this.dependencies[relativeFile][value] = {
161+
unsafe: Boolean(metadata?.unsafe),
162+
inTry: Boolean(metadata?.inTry)
163+
};
164+
165+
if (metadata?.inTry) {
166+
this.#dependenciesInTryBlock.add(value);
167+
}
168+
const filtered = this.#filerDependencyByKind(value, relativeFile);
169+
170+
if (filtered.file) {
171+
this.#files.add(filtered.file);
172+
}
173+
if (filtered.package) {
174+
this.#analyzeDependency(filtered.package, Boolean(metadata?.inTry));
175+
}
176+
this.#values.add(value);
177+
}
178+
179+
#filerDependencyByKind(dependency: string, relativeFileLocation: string) {
180+
const firstChar = dependency.charAt(0);
181+
182+
/**
183+
* @example
184+
* require("..");
185+
* require("/home/marco/foo.js");
186+
*/
187+
if (firstChar === "." || firstChar === "/") {
188+
// Note: condition only possible for CJS
189+
if (kRelativeImportPath.has(dependency)) {
190+
return { file: path.join(dependency, "index.js") };
191+
}
192+
193+
// Note: we are speculating that the extension is .js (but it could be .json or .node)
194+
const fixedFileName = path.extname(dependency) === "" ?
195+
`${dependency}.js` : dependency;
196+
197+
return { file: path.join(relativeFileLocation, fixedFileName) };
198+
}
199+
200+
return { package: dependency };
201+
}
202+
relativeFileLocation: string;
203+
204+
#analyzeDependency(sourceDependency: string, inTry: boolean) {
205+
if (this.#values.has(sourceDependency)) {
206+
return;
207+
}
208+
const { dependencies, devDependencies, nodejsImports = {} } = this.#mama;
209+
let thirdPartyAliasedDependency: string | undefined;
210+
// See: https://nodejs.org/api/packages.html#subpath-imports
211+
if (this.#isAliasFileModule(sourceDependency) && sourceDependency in nodejsImports) {
212+
const [alias, importEntry] = this.#buildSubpathDependency(sourceDependency, nodejsImports);
213+
this.#subpathImportsDependencies[alias] = importEntry;
214+
if (!this.#isFile(importEntry)) {
215+
this.#thirdPartyAliasedDependencies.add(importEntry);
216+
thirdPartyAliasedDependency = importEntry;
217+
}
218+
}
219+
220+
const name = dependencies.includes(sourceDependency) ?
221+
sourceDependency :
222+
parseNpmSpec(sourceDependency)?.name ?? sourceDependency;
223+
224+
let thirdPartyDependency: string | undefined;
225+
226+
if (!this.#isFile(name) &&
227+
!this.#isCoreModule(name) &&
228+
!devDependencies.includes(name)
229+
&& !inTry
230+
) {
231+
thirdPartyDependency = name;
232+
this.#thirdPartyDependencies.add(name);
233+
}
234+
235+
if (thirdPartyDependency && this.#isMissingDependency(thirdPartyDependency, thirdPartyAliasedDependency)) {
236+
this.#missingDependencies.add(thirdPartyDependency);
237+
}
238+
239+
let isNodeDependency = false;
240+
241+
if (this.#isCoreModule(sourceDependency)) {
242+
this.#nodeDependencies.add(sourceDependency);
243+
isNodeDependency = true;
244+
}
245+
246+
if (this.#hasExternalCapacity) {
247+
return;
248+
}
249+
250+
if (((isNodeDependency && kExternalModules.has(sourceDependency))
251+
|| (thirdPartyDependency && kExternalThirdPartyDeps.has(thirdPartyDependency)))) {
252+
this.#hasExternalCapacity = true;
253+
}
254+
}
255+
256+
#isMissingDependency(thirdPartyDependency: string, thirdPartyAliasedDependency: string | undefined) {
257+
const { dependencies, nodejsImports = {} } = this.#mama;
258+
259+
return !dependencies.includes(thirdPartyDependency) &&
260+
!(thirdPartyDependency in nodejsImports) &&
261+
thirdPartyDependency !== thirdPartyAliasedDependency;
262+
}
263+
264+
#difference<T>(arr1: T[], arr2: T[]): T[] {
265+
return arr1.filter((item) => !arr2.includes(item));
266+
}
267+
268+
#isFile(
269+
filePath: string
270+
) {
271+
return filePath.startsWith(".")
272+
|| kFileExtensions.some((extension) => filePath.endsWith(extension));
273+
}
274+
275+
#isCoreModule(
276+
moduleName: string
277+
): boolean {
278+
const cleanModuleName = moduleName.startsWith("node:") ? moduleName.slice(5) : moduleName;
279+
280+
// Note: We need to also check moduleName because builtins package only return true for 'node:test'.
281+
return NODE_BUILTINS.has(cleanModuleName) || NODE_BUILTINS.has(moduleName);
282+
}
283+
284+
#isAliasFileModule(
285+
moduleName: string
286+
): moduleName is `#${string}` {
287+
return moduleName.charAt(0) === "#";
288+
}
289+
290+
#buildSubpathDependency(
291+
alias: string,
292+
nodeImports: Record<string, string | NodeImport>
293+
): [string, string] {
294+
const importEntry = nodeImports[alias]!;
295+
296+
return typeof importEntry === "string" ?
297+
[alias, importEntry] :
298+
[alias, "node" in importEntry ? importEntry.node : importEntry.default];
299+
}
300+
301+
values() {
302+
return this.#values;
303+
}
304+
}

0 commit comments

Comments
 (0)