@nodesecure/scanner 2.0.0 → 3.0.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.
@@ -0,0 +1,57 @@
1
+ // Import Node.js Dependencies
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ // Import Third-party Dependencies
6
+ import { parseManifestAuthor } from "@nodesecure/utils";
7
+
8
+ // CONSTANTS
9
+ // PR welcome to contribute to this list!
10
+ const kNativeNpmPackages = new Set([
11
+ "node-gyp", "node-pre-gyp", "node-gyp-build", "node-addon-api"
12
+ ]);
13
+
14
+ /**
15
+ * @see https://www.nerdycode.com/prevent-npm-executing-scripts-security/
16
+ */
17
+ const kUnsafeNpmScripts = new Set([
18
+ "install",
19
+ "preinstall", "postinstall",
20
+ "preuninstall", "postuninstall"
21
+ ]);
22
+
23
+ /**
24
+ * @param {!string} location
25
+ * @returns {import("@npm/types").PackageJson}
26
+ */
27
+ export async function read(location) {
28
+ const packageStr = await fs.readFile(
29
+ path.join(location, "package.json"),
30
+ "utf-8"
31
+ );
32
+
33
+ return JSON.parse(packageStr);
34
+ }
35
+
36
+ // TODO: PR @npm/types to fix dependencies typo
37
+ export async function readAnalyze(location) {
38
+ const {
39
+ description = "", author = {}, scripts = {},
40
+ dependencies = {}, devDependencies = {}, gypfile = false
41
+ } = await read(location);
42
+
43
+ const packageDeps = Object.keys(dependencies);
44
+ const packageDevDeps = Object.keys(devDependencies);
45
+ const hasNativePackage = [...packageDevDeps, ...packageDeps]
46
+ .some((pkg) => kNativeNpmPackages.has(pkg));
47
+
48
+ return {
49
+ author: typeof author === "string" ? parseManifestAuthor(author) : author,
50
+ description,
51
+ hasScript: Object.keys(scripts)
52
+ .some((value) => kUnsafeNpmScripts.has(value.toLowerCase())),
53
+ packageDeps,
54
+ packageDevDeps,
55
+ hasNativeElements: hasNativePackage || gypfile
56
+ };
57
+ }
@@ -0,0 +1,68 @@
1
+ // Import Third-party Dependencies
2
+ import semver from "semver";
3
+ import { parseManifestAuthor } from "@nodesecure/utils";
4
+ import { packument } from "@nodesecure/npm-registry-sdk";
5
+
6
+ export function parseAuthor(author) {
7
+ return typeof author === "string" ? parseManifestAuthor(author) : author;
8
+ }
9
+
10
+ export async function packageMetadata(name, version, options) {
11
+ const { ref, logger } = options;
12
+
13
+ try {
14
+ const pkg = await packument(name);
15
+
16
+ const oneYearFromToday = new Date();
17
+ oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() - 1);
18
+
19
+ const lastVersion = pkg["dist-tags"].latest;
20
+ const lastUpdateAt = new Date(pkg.time[lastVersion]);
21
+ const metadata = {
22
+ author: parseAuthor(pkg.author) ?? {},
23
+ homepage: pkg.homepage || null,
24
+ publishedCount: Object.values(pkg.versions).length,
25
+ lastVersion,
26
+ lastUpdateAt,
27
+ hasReceivedUpdateInOneYear: !(oneYearFromToday > lastUpdateAt),
28
+ maintainers: pkg.maintainers,
29
+ publishers: []
30
+ };
31
+
32
+ const isOutdated = semver.neq(version, lastVersion);
33
+ if (isOutdated) {
34
+ ref.versions[version].flags.push("isOutdated");
35
+ }
36
+
37
+ const publishers = new Set();
38
+ for (const ver of Object.values(pkg.versions).reverse()) {
39
+ const { _npmUser: npmUser, version } = ver;
40
+ const isNullOrUndefined = typeof npmUser === "undefined" || npmUser === null;
41
+ if (isNullOrUndefined || !("name" in npmUser) || typeof npmUser.name !== "string") {
42
+ continue;
43
+ }
44
+
45
+ const authorName = metadata.author?.name ?? null;
46
+ if (authorName === null) {
47
+ metadata.author = npmUser;
48
+ }
49
+ else if (npmUser.name !== metadata.author.name) {
50
+ metadata.hasManyPublishers = true;
51
+ }
52
+
53
+ // TODO: add npmUser.email
54
+ if (!publishers.has(npmUser.name)) {
55
+ publishers.add(npmUser.name);
56
+ metadata.publishers.push({ ...npmUser, version, at: new Date(pkg.time[version]) });
57
+ }
58
+ }
59
+
60
+ Object.assign(ref.metadata, metadata);
61
+ }
62
+ catch {
63
+ // ignore
64
+ }
65
+ finally {
66
+ logger.tick("registry");
67
+ }
68
+ }
package/src/tarball.js CHANGED
@@ -1,99 +1,63 @@
1
1
  // Import Node.js Dependencies
2
- import { join, extname, dirname } from "path";
3
- import fs from "fs/promises";
2
+ import path from "path";
4
3
  import os from "os";
5
4
  import timers from "timers/promises";
6
5
 
7
6
  // Import Third-party Dependencies
8
- import { runASTAnalysis } from "@nodesecure/js-x-ray";
9
- import difference from "lodash.difference";
10
- import isMinified from "is-minified-code";
7
+ import { runASTAnalysisOnFile } from "@nodesecure/js-x-ray";
11
8
  import pacote from "pacote";
12
9
  import ntlp from "@nodesecure/ntlp";
13
- import builtins from "builtins";
14
10
 
15
11
  // Import Internal Dependencies
16
- import { getTarballComposition, isSensitiveFile, getPackageName, constants } from "./utils/index.js";
12
+ import {
13
+ getTarballComposition, isSensitiveFile, filterDependencyKind, analyzeDependencies, booleanToFlags,
14
+ NPM_TOKEN
15
+ } from "./utils/index.js";
16
+ import * as manifest from "./manifest.js";
17
17
  import { getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
18
18
 
19
19
  // CONSTANTS
20
- const DIRECT_PATH = new Set([".", "..", "./", "../"]);
21
- const NATIVE_CODE_EXTENSIONS = new Set([".gyp", ".c", ".cpp", ".node", ".so", ".h"]);
22
- const NATIVE_NPM_PACKAGES = new Set(["node-gyp", "node-pre-gyp", "node-gyp-build", "node-addon-api"]);
23
- const NODE_CORE_LIBS = new Set(builtins({ experimental: true }));
24
-
25
- export async function readManifest(dest, ref) {
26
- const packageStr = await fs.readFile(join(dest, "package.json"), "utf-8");
27
- const packageJSON = JSON.parse(packageStr);
28
- const {
29
- description = "", author = {}, scripts = {}, dependencies = {}, devDependencies = {}, gypfile = false
30
- } = packageJSON;
31
-
32
- ref.description = description;
33
- ref.author = author;
34
-
35
- // TODO: handle this to @nodesecure/flags
36
- ref.flags.hasScript = [...Object.keys(scripts)]
37
- .some((value) => constants.NPM_SCRIPTS.has(value.toLowerCase()));
20
+ const kNativeCodeExtensions = new Set([".gyp", ".c", ".cpp", ".node", ".so", ".h"]);
21
+ const kJsExtname = new Set([".js", ".mjs", ".cjs"]);
22
+
23
+ export async function scanJavascriptFile(dest, file, packageName) {
24
+ const result = await runASTAnalysisOnFile(path.join(dest, file), { packageName });
25
+
26
+ const warnings = result.warnings.map((curr) => Object.assign({}, curr, { file }));
27
+ if (!result.ok) {
28
+ return {
29
+ file,
30
+ warnings,
31
+ isMinified: false,
32
+ tryDependencies: [],
33
+ dependencies: [],
34
+ filesDependencies: []
35
+ };
36
+ }
37
+ const { packages, files } = filterDependencyKind(result.dependencies, path.dirname(file));
38
38
 
39
39
  return {
40
- packageDeps: [...Object.keys(dependencies)],
41
- packageDevDeps: Object.keys(devDependencies),
42
- packageGyp: gypfile
40
+ file,
41
+ warnings,
42
+ isMinified: result.isMinified,
43
+ tryDependencies: [...result.dependencies.getDependenciesInTryStatement()],
44
+ dependencies: packages,
45
+ filesDependencies: files
43
46
  };
44
47
  }
45
48
 
46
- export async function scanFile(dest, file, options) {
47
- const { name, ref } = options;
48
-
49
- try {
50
- const str = await fs.readFile(join(dest, file), "utf-8");
51
- const isMin = file.includes(".min") || isMinified(str);
52
-
53
- const ASTAnalysis = runASTAnalysis(str, { isMinified: isMin });
54
- ASTAnalysis.dependencies.removeByName(name);
55
-
56
- const dependencies = [];
57
- const filesDependencies = [];
58
- for (const depName of ASTAnalysis.dependencies) {
59
- if (depName.startsWith(".")) {
60
- const indexName = DIRECT_PATH.has(depName) ? join(depName, "index.js") : join(dirname(file), depName);
61
- filesDependencies.push(indexName);
62
- }
63
- else {
64
- dependencies.push(depName);
65
- }
66
- }
67
- const inTryDeps = [...ASTAnalysis.dependencies.getDependenciesInTryStatement()];
68
-
69
- if (!ASTAnalysis.isOneLineRequire && isMin) {
70
- ref.composition.minified.push(file);
71
- }
72
- ref.warnings.push(...ASTAnalysis.warnings.map((curr) => Object.assign({}, curr, { file })));
73
-
74
- return { inTryDeps, dependencies, filesDependencies };
75
- }
76
- catch (error) {
77
- if (!("code" in error)) {
78
- ref.warnings.push({ file, kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] });
79
- }
80
-
81
- return null;
82
- }
83
- }
84
-
85
49
  export async function scanDirOrArchive(name, version, options) {
86
50
  const { ref, tmpLocation, locker } = options;
87
51
 
88
52
  const isNpmTarball = !(tmpLocation === null);
89
- const dest = isNpmTarball ? join(tmpLocation, `${name}@${version}`) : process.cwd();
53
+ const dest = isNpmTarball ? path.join(tmpLocation, `${name}@${version}`) : process.cwd();
90
54
  const free = await locker.acquireOne();
91
55
 
92
56
  try {
93
57
  // If this is an NPM tarball then we extract it on the disk with pacote.
94
58
  if (isNpmTarball) {
95
59
  await pacote.extract(ref.flags.includes("isGit") ? ref.gitUrl : `${name}@${version}`, dest, {
96
- ...constants.NPM_TOKEN,
60
+ ...NPM_TOKEN,
97
61
  registry: getLocalRegistryURL(),
98
62
  cache: `${os.homedir()}/.npm`
99
63
  });
@@ -101,103 +65,67 @@ export async function scanDirOrArchive(name, version, options) {
101
65
  }
102
66
 
103
67
  // Read the package.json at the root of the directory or archive.
104
- const { packageDeps, packageDevDeps, packageGyp } = await readManifest(dest, ref);
68
+ const { packageDeps, packageDevDeps, author, description, hasScript, hasNativeElements } = await manifest.readAnalyze(dest);
69
+ ref.author = author;
70
+ ref.description = description;
105
71
 
106
72
  // Get the composition of the (extracted) directory
107
73
  const { ext, files, size } = await getTarballComposition(dest);
108
74
  ref.size = size;
109
75
  ref.composition.extensions.push(...ext);
110
76
  ref.composition.files.push(...files);
111
- if (files.some((path) => isSensitiveFile(path))) {
112
- ref.flags.push("hasBannedFile");
113
- }
77
+ const hasBannedFile = files.some((path) => isSensitiveFile(path));
78
+ const hasNativeCode = hasNativeElements || files.some((file) => kNativeCodeExtensions.has(path.extname(file)));
114
79
 
115
80
  // Search for minified and runtime dependencies
116
81
  // Run a JS-X-Ray analysis on each JavaScript files of the project!
117
- const [dependencies, filesDependencies, inTryDeps] = [new Set(), new Set(), new Set()];
118
- const fileAnalysisResults = await Promise.all(
82
+ const fileAnalysisRaw = await Promise.allSettled(
119
83
  files
120
- .filter((name) => constants.EXT_JS.has(extname(name)))
121
- .map((file) => scanFile(dest, file, { name, ref }))
84
+ .filter((name) => kJsExtname.has(path.extname(name)))
85
+ .map((file) => scanJavascriptFile(dest, file, name))
122
86
  );
123
87
 
124
- for (const result of fileAnalysisResults.filter((row) => row !== null)) {
125
- result.inTryDeps.forEach((dep) => inTryDeps.add(dep));
126
- result.dependencies.forEach((dep) => dependencies.add(dep));
127
- result.filesDependencies.forEach((dep) => filesDependencies.add(dep));
128
- }
129
-
130
- // Search for native code
131
- {
132
- const hasNativeFile = files.some((file) => NATIVE_CODE_EXTENSIONS.has(extname(file)));
133
- const hasNativePackage = hasNativeFile ? null : [
134
- ...new Set([...packageDevDeps, ...(packageDeps || [])])
135
- ].some((pkg) => NATIVE_NPM_PACKAGES.has(pkg));
88
+ const fileAnalysisResults = fileAnalysisRaw
89
+ .filter((promiseSettledResult) => promiseSettledResult.status === "fulfilled")
90
+ .map((promiseSettledResult) => promiseSettledResult.value);
136
91
 
137
- if (hasNativeFile || hasNativePackage || packageGyp) {
138
- ref.flags.push("hasNativeCode");
139
- }
140
- }
92
+ ref.warnings.push(...fileAnalysisResults.flatMap((row) => row.warnings));
141
93
 
142
- if (ref.warnings.length > 0 && !ref.flags.includes("hasWarnings")) {
143
- ref.flags.push("hasWarnings");
144
- }
145
- const required = [...dependencies];
94
+ const dependencies = [...new Set(fileAnalysisResults.flatMap((row) => row.dependencies))];
95
+ const filesDependencies = [...new Set(fileAnalysisResults.flatMap((row) => row.filesDependencies))];
96
+ const tryDependencies = new Set(fileAnalysisResults.flatMap((row) => row.tryDependencies));
97
+ const minifiedFiles = fileAnalysisResults.filter((row) => row.isMinified).flatMap((row) => row.file);
146
98
 
147
- if (packageDeps !== null) {
148
- const thirdPartyDependencies = required
149
- .map((name) => (packageDeps.includes(name) ? name : getPackageName(name)))
150
- .filter((name) => !name.startsWith("."))
151
- .filter((name) => !NODE_CORE_LIBS.has(name))
152
- .filter((name) => !packageDevDeps.includes(name))
153
- .filter((name) => !inTryDeps.has(name));
154
- ref.composition.required_thirdparty = thirdPartyDependencies;
99
+ const {
100
+ nodeDependencies, thirdPartyDependencies, missingDependencies, unusedDependencies, flags
101
+ } = analyzeDependencies(dependencies, { packageDeps, packageDevDeps, tryDependencies });
155
102
 
156
- const unusedDeps = difference(
157
- packageDeps.filter((name) => !name.startsWith("@types")), thirdPartyDependencies);
158
- const missingDeps = new Set(difference(thirdPartyDependencies, packageDeps));
159
-
160
- if (unusedDeps.length > 0 || missingDeps.length > 0) {
161
- ref.flags.push("hasMissingOrUnusedDependency");
162
- }
163
- ref.composition.unused.push(...unusedDeps);
164
- ref.composition.missing.push(...missingDeps);
165
- }
166
-
167
- ref.composition.required_files = [...filesDependencies]
168
- .filter((depName) => depName.trim() !== "")
169
- // .map((depName) => {
170
- // return files.includes(depName) ? depName : join(depName, "index.js");
171
- // })
172
- .map((depName) => (extname(depName) === "" ? `${depName}.js` : depName));
173
- ref.composition.required_nodejs = required.filter((name) => NODE_CORE_LIBS.has(name));
174
-
175
- if (ref.composition.minified.length > 0) {
176
- ref.flags.push("hasMinifiedCode");
177
- }
178
-
179
- const hasExternalCapacity = ref.composition.required_nodejs
180
- .some((depName) => constants.EXT_DEPS.has(depName));
181
- if (hasExternalCapacity) {
182
- ref.flags.push("hasExternalCapacity");
183
- }
103
+ ref.composition.required_thirdparty = thirdPartyDependencies;
104
+ ref.composition.unused.push(...unusedDependencies);
105
+ ref.composition.missing.push(...missingDependencies);
106
+ ref.composition.required_files = filesDependencies;
107
+ ref.composition.required_nodejs = nodeDependencies;
108
+ ref.composition.minified = minifiedFiles;
184
109
 
185
110
  // License
186
111
  await timers.setImmediate();
187
112
  const licenses = await ntlp(dest);
188
-
189
113
  const uniqueLicenseIds = Array.isArray(licenses.uniqueLicenseIds) ? licenses.uniqueLicenseIds : [];
190
- if (uniqueLicenseIds.length === 0) {
191
- ref.flags.push("hasNoLicense");
192
- }
193
- if (licenses.hasMultipleLicenses) {
194
- ref.flags.push("hasMultipleLicenses");
195
- }
196
-
197
114
  ref.license = licenses;
198
115
  ref.license.uniqueLicenseIds = uniqueLicenseIds;
116
+
117
+ ref.flags.push(...booleanToFlags({
118
+ ...flags,
119
+ hasNoLicense: uniqueLicenseIds.length === 0,
120
+ hasMultipleLicenses: licenses.hasMultipleLicenses,
121
+ hasMinifiedCode: minifiedFiles.length > 0,
122
+ hasWarnings: ref.warnings.length > 0 && !ref.flags.includes("hasWarnings"),
123
+ hasBannedFile,
124
+ hasNativeCode,
125
+ hasScript
126
+ }));
199
127
  }
200
- catch (err) {
128
+ catch {
201
129
  // Ignore
202
130
  }
203
131
  finally {
@@ -205,66 +133,35 @@ export async function scanDirOrArchive(name, version, options) {
205
133
  }
206
134
  }
207
135
 
208
- async function readJSFile(dest, file) {
209
- const str = await fs.readFile(join(dest, file), "utf-8");
210
-
211
- return [file, str];
212
- }
213
-
214
136
  export async function scanPackage(dest, packageName) {
215
- // Read the package.json file inside the extracted directory.
216
- let isProjectUsingESM = false;
217
- let localPackageName = packageName;
218
- {
219
- const packageStr = await fs.readFile(join(dest, "package.json"), "utf-8");
220
- const { type = "script", name } = JSON.parse(packageStr);
221
- isProjectUsingESM = type === "module";
222
- if (localPackageName === null) {
223
- localPackageName = name;
224
- }
225
- }
137
+ const { type = "script", name } = await manifest.read(dest);
226
138
 
227
- // Get the tarball composition
228
139
  await timers.setImmediate();
229
140
  const { ext, files, size } = await getTarballComposition(dest);
141
+ ext.delete("");
230
142
 
231
143
  // Search for runtime dependencies
232
144
  const dependencies = Object.create(null);
233
- const minified = [];
234
- const warnings = [];
145
+ const [minified, warnings] = [[], []];
235
146
 
236
- const JSFiles = files.filter((name) => constants.EXT_JS.has(extname(name)));
237
- const allFilesContent = (await Promise.allSettled(JSFiles.map((file) => readJSFile(dest, file))))
238
- .filter((_p) => _p.status === "fulfilled").map((_p) => _p.value);
147
+ const JSFiles = files.filter((name) => kJsExtname.has(path.extname(name)));
148
+ for (const file of JSFiles) {
149
+ const result = await runASTAnalysisOnFile(path.join(dest, file), {
150
+ packageName: packageName ?? name,
151
+ module: type === "module"
152
+ });
239
153
 
240
- // TODO: 2) handle dependency by file to not loose data.
241
- for (const [file, str] of allFilesContent) {
242
- try {
243
- const ASTAnalysis = runASTAnalysis(str, {
244
- module: extname(file) === ".mjs" ? true : isProjectUsingESM
245
- });
246
- ASTAnalysis.dependencies.removeByName(localPackageName);
247
- dependencies[file] = ASTAnalysis.dependencies.dependencies;
248
- warnings.push(...ASTAnalysis.warnings.map((warn) => {
249
- warn.file = file;
250
-
251
- return warn;
252
- }));
253
-
254
- if (!ASTAnalysis.isOneLineRequire && !file.includes(".min") && isMinified(str)) {
255
- minified.push(file);
256
- }
257
- }
258
- catch (err) {
259
- if (!Reflect.has(err, "code")) {
260
- warnings.push({ file, kind: "parsing-error", value: err.message, location: [[0, 0], [0, 0]] });
261
- }
154
+ warnings.push(...result.warnings.map((curr) => Object.assign({}, curr, { file })));
155
+ if (!result.ok) {
156
+ continue;
262
157
  }
158
+
159
+ dependencies[file] = result.dependencies.dependencies;
160
+ result.isMinified && minified.push(file);
263
161
  }
264
162
 
265
163
  await timers.setImmediate();
266
164
  const { uniqueLicenseIds, licenses } = await ntlp(dest);
267
- ext.delete("");
268
165
 
269
166
  return {
270
167
  files: { list: files, extensions: [...ext], minified },
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @param {Set<string>} flags
3
+ * @param {import("../../types/scanner").Dependency} descriptor
4
+ */
5
+ export function* addMissingVersionFlags(flags, descriptor) {
6
+ const { metadata, vulnerabilities = [], versions } = descriptor;
7
+ const semverVersions = Object.keys(versions);
8
+
9
+ if (!metadata.hasReceivedUpdateInOneYear && flags.has("hasOutdatedDependency") && !flags.has("isDead")) {
10
+ yield "isDead";
11
+ }
12
+ if (metadata.hasManyPublishers && !flags.has("hasManyPublishers")) {
13
+ yield "hasManyPublishers";
14
+ }
15
+ if (metadata.hasChangedAuthor && !flags.has("hasChangedAuthor")) {
16
+ yield "hasChangedAuthor";
17
+ }
18
+ if (vulnerabilities.length > 0 && !flags.has("hasVulnerabilities")) {
19
+ yield "hasVulnerabilities";
20
+ }
21
+ if (semverVersions.length > 1 && !flags.has("hasDuplicate")) {
22
+ yield "hasDuplicate";
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ // Import Third-party Dependencies
2
+ import difference from "lodash.difference";
3
+ import builtins from "builtins";
4
+
5
+ // Import Internal Dependencies
6
+ import { getPackageName } from "./getPackageName.js";
7
+
8
+ // CONSTANTS
9
+ const kNodeModules = new Set(builtins({ experimental: true }));
10
+ const kExternalModules = new Set(["http", "https", "net", "http2", "dgram", "child_process"]);
11
+
12
+ export function analyzeDependencies(dependencies, deps = {}) {
13
+ const { packageDeps, packageDevDeps, tryDependencies } = deps;
14
+
15
+ const thirdPartyDependencies = dependencies
16
+ .map((name) => (packageDeps.includes(name) ? name : getPackageName(name)))
17
+ .filter((name) => !name.startsWith("."))
18
+ .filter((name) => !kNodeModules.has(name))
19
+ .filter((name) => !packageDevDeps.includes(name))
20
+ .filter((name) => !tryDependencies.has(name));
21
+
22
+ const unusedDependencies = difference(
23
+ packageDeps.filter((name) => !name.startsWith("@types")),
24
+ thirdPartyDependencies
25
+ );
26
+ const missingDependencies = [...new Set(difference(thirdPartyDependencies, packageDeps))];
27
+ const nodeDependencies = dependencies.filter((name) => kNodeModules.has(name));
28
+
29
+ return {
30
+ nodeDependencies,
31
+ thirdPartyDependencies,
32
+ unusedDependencies,
33
+ missingDependencies,
34
+
35
+ flags: {
36
+ hasExternalCapacity: nodeDependencies.some((depName) => kExternalModules.has(depName)),
37
+ hasMissingOrUnusedDependency: unusedDependencies.length > 0 || missingDependencies.length > 0
38
+ }
39
+ };
40
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @param {Record<string, boolean>} flagsRecord
3
+ * @example
4
+ * console.log(...booleanToFlags({ hasScript: true })); // "hasScript"
5
+ */
6
+ export function* booleanToFlags(flagsRecord) {
7
+ for (const [flagName, boolValue] of Object.entries(flagsRecord)) {
8
+ if (boolValue) {
9
+ yield flagName;
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,44 @@
1
+ // Import Node.js Dependencies
2
+ import path from "path";
3
+
4
+ // CONSTANTS
5
+ const kRelativeImportPath = new Set([".", "..", "./", "../"]);
6
+
7
+ /**
8
+ * @see https://nodejs.org/docs/latest/api/modules.html#file-modules
9
+ *
10
+ * @param {IterableIterator<string>} dependencies
11
+ * @param {!string} relativeFileLocation
12
+ */
13
+ export function filterDependencyKind(dependencies, relativeFileLocation) {
14
+ const packages = [];
15
+ const files = [];
16
+
17
+ for (const moduleNameOrPath of dependencies) {
18
+ const firstChar = moduleNameOrPath.charAt(0);
19
+
20
+ /**
21
+ * @example
22
+ * require("..");
23
+ * require("/home/marco/foo.js");
24
+ */
25
+ if (firstChar === "." || firstChar === "/") {
26
+ // Note: condition only possible for CJS
27
+ if (kRelativeImportPath.has(moduleNameOrPath)) {
28
+ files.push(path.join(moduleNameOrPath, "index.js"));
29
+ }
30
+ else {
31
+ // Note: we are speculating that the extension is .js (but it could be .json or .node)
32
+ const fixedFileName = path.extname(moduleNameOrPath) === "" ?
33
+ `${moduleNameOrPath}.js` : moduleNameOrPath;
34
+
35
+ files.push(path.join(relativeFileLocation, fixedFileName));
36
+ }
37
+ }
38
+ else {
39
+ packages.push(moduleNameOrPath);
40
+ }
41
+ }
42
+
43
+ return { packages, files };
44
+ }
@@ -1,5 +1,21 @@
1
+ // CONSTANTS
2
+ const kPackageSeparator = "/";
3
+ const kPackageOrgSymbol = "@";
4
+
5
+ /**
6
+ * @see https://github.com/npm/validate-npm-package-name#naming-rules
7
+ *
8
+ * @param {!string} name full package name
9
+ * @returns {string}
10
+ *
11
+ * @example
12
+ * getPackageName("foo"); // foo
13
+ * getPackageName("foo/bar"); // foo
14
+ * getPackageName("@org/bar"); // @org/bar
15
+ */
1
16
  export function getPackageName(name) {
2
- const parts = name.split("/");
17
+ const parts = name.split(kPackageSeparator);
3
18
 
4
- return name.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
19
+ // Note: only scoped package are allowed to start with @
20
+ return name.startsWith(kPackageOrgSymbol) ? `${parts[0]}/${parts[1]}` : parts[0];
5
21
  }
@@ -21,16 +21,14 @@ export async function getTarballComposition(tarballDir) {
21
21
  }
22
22
  }
23
23
 
24
- try {
25
- const sizeAll = await Promise.all([
26
- ...files.map((file) => fs.stat(file)),
27
- ...dirs.map((file) => fs.stat(file))
28
- ]);
29
- size += sizeAll.reduce((prev, curr) => prev + curr.size, 0);
30
- }
31
- catch (err) {
32
- // ignore
33
- }
24
+ const sizeUnfilteredResult = await Promise.allSettled([
25
+ ...files.map((file) => fs.stat(file)),
26
+ ...dirs.map((file) => fs.stat(file))
27
+ ]);
28
+ const sizeAll = sizeUnfilteredResult
29
+ .filter((promiseSettledResult) => promiseSettledResult.status === "fulfilled")
30
+ .map((promiseSettledResult) => promiseSettledResult.value);
31
+ size += sizeAll.reduce((prev, curr) => prev + curr.size, 0);
34
32
 
35
33
  return {
36
34
  ext,