@node-cli/bundlecheck 1.3.0 → 1.4.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/dist/bundler.js CHANGED
@@ -8,12 +8,12 @@ import * as esbuild from "esbuild";
8
8
  import { DEFAULT_EXTERNALS } from "./defaults.js";
9
9
  const gzipAsync = promisify(zlib.gzip);
10
10
  /**
11
- * Escape special regex characters in a string
11
+ * Escape special regex characters in a string.
12
12
  */ function escapeRegExp(str) {
13
13
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
14
  }
15
15
  /**
16
- * Parse a package specifier to extract name, version, and subpath
16
+ * Parse a package specifier to extract name, version, and subpath.
17
17
  * Handles:
18
18
  * - @scope/package@1.0.0
19
19
  * - @scope/package/subpath@1.0.0
@@ -24,17 +24,18 @@ const gzipAsync = promisify(zlib.gzip);
24
24
  let version = "latest";
25
25
  // Handle scoped packages (@scope/name...)
26
26
  if (workingSpec.startsWith("@")) {
27
- // Find the second @ which would separate version
27
+ // Find the second @ which would separate version.
28
28
  const secondAtIndex = workingSpec.indexOf("@", 1);
29
29
  if (secondAtIndex !== -1) {
30
30
  version = workingSpec.substring(secondAtIndex + 1);
31
31
  workingSpec = workingSpec.substring(0, secondAtIndex);
32
32
  }
33
- // Now workingSpec is like @scope/name or @scope/name/subpath
34
- // Split by / and check if there are more than 2 parts
35
- const parts = workingSpec.split("/");
33
+ /**
34
+ * Now workingSpec is like @scope/name or @scope/name/subpath Split by / and
35
+ * check if there are more than 2 parts.
36
+ */ const parts = workingSpec.split("/");
36
37
  if (parts.length > 2) {
37
- // Has subpath: @scope/name/subpath/more
38
+ // Has subpath: @scope/name/subpath/more.
38
39
  const name = `${parts[0]}/${parts[1]}`;
39
40
  const subpath = parts.slice(2).join("/");
40
41
  return {
@@ -43,19 +44,19 @@ const gzipAsync = promisify(zlib.gzip);
43
44
  subpath
44
45
  };
45
46
  }
46
- // No subpath: @scope/name
47
+ // No subpath: @scope/name.
47
48
  return {
48
49
  name: workingSpec,
49
50
  version
50
51
  };
51
52
  }
52
- // Handle non-scoped packages (name@version or name/subpath@version)
53
+ // Handle non-scoped packages (name@version or name/subpath@version).
53
54
  const atIndex = workingSpec.indexOf("@");
54
55
  if (atIndex !== -1) {
55
56
  version = workingSpec.substring(atIndex + 1);
56
57
  workingSpec = workingSpec.substring(0, atIndex);
57
58
  }
58
- // Check for subpath in non-scoped packages
59
+ // Check for subpath in non-scoped packages.
59
60
  const slashIndex = workingSpec.indexOf("/");
60
61
  if (slashIndex !== -1) {
61
62
  const name = workingSpec.substring(0, slashIndex);
@@ -72,7 +73,7 @@ const gzipAsync = promisify(zlib.gzip);
72
73
  };
73
74
  }
74
75
  /**
75
- * Format bytes to human-readable string
76
+ * Format bytes to human-readable string.
76
77
  */ export function formatBytes(bytes) {
77
78
  if (bytes === 0) {
78
79
  return "0 B";
@@ -88,7 +89,7 @@ const gzipAsync = promisify(zlib.gzip);
88
89
  return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
89
90
  }
90
91
  /**
91
- * Create a temporary directory for bundling
92
+ * Create a temporary directory for bundling.
92
93
  */ function createTempDir() {
93
94
  const tmpDir = path.join(os.tmpdir(), `bundlecheck-${Date.now()}`);
94
95
  fs.mkdirSync(tmpDir, {
@@ -97,7 +98,7 @@ const gzipAsync = promisify(zlib.gzip);
97
98
  return tmpDir;
98
99
  }
99
100
  /**
100
- * Clean up temporary directory
101
+ * Clean up temporary directory.
101
102
  */ function cleanupTempDir(tmpDir) {
102
103
  try {
103
104
  fs.rmSync(tmpDir, {
@@ -105,11 +106,11 @@ const gzipAsync = promisify(zlib.gzip);
105
106
  force: true
106
107
  });
107
108
  } catch {
108
- // Ignore cleanup errors
109
+ // Ignore cleanup errors.
109
110
  }
110
111
  }
111
112
  /**
112
- * Check if pnpm is available
113
+ * Check if pnpm is available.
113
114
  */ function isPnpmAvailable() {
114
115
  try {
115
116
  execSync("pnpm --version", {
@@ -120,30 +121,30 @@ const gzipAsync = promisify(zlib.gzip);
120
121
  return false;
121
122
  }
122
123
  }
123
- // Cache the result of pnpm availability check
124
+ // Cache the result of pnpm availability check.
124
125
  let usePnpm = null;
125
126
  /**
126
- * Validate and sanitize a registry URL to prevent command injection
127
+ * Validate and sanitize a registry URL to prevent command injection.
127
128
  * @param registry - The registry URL to validate
128
129
  * @returns The sanitized URL or undefined if invalid
129
130
  * @throws Error if the URL is invalid or contains potentially malicious characters
130
131
  */ function validateRegistryUrl(registry) {
131
- // Parse as URL to validate format
132
+ // Parse as URL to validate format.
132
133
  let url;
133
134
  try {
134
135
  url = new URL(registry);
135
136
  } catch {
136
137
  throw new Error(`Invalid registry URL: ${registry}. Must be a valid URL (e.g., https://registry.example.com)`);
137
138
  }
138
- // Only allow http and https protocols
139
+ // Only allow http and https protocols.
139
140
  if (url.protocol !== "http:" && url.protocol !== "https:") {
140
141
  throw new Error(`Invalid registry URL protocol: ${url.protocol}. Only http: and https: are allowed`);
141
142
  }
142
- // Return the sanitized URL (URL constructor normalizes it)
143
+ // Return the sanitized URL (URL constructor normalizes it).
143
144
  return url.toString();
144
145
  }
145
146
  /**
146
- * Get the install command (pnpm preferred, npm fallback)
147
+ * Get the install command (pnpm preferred, npm fallback).
147
148
  * @param registry - Optional custom npm registry URL
148
149
  */ function getInstallCommand(registry) {
149
150
  if (usePnpm === null) {
@@ -151,9 +152,9 @@ let usePnpm = null;
151
152
  }
152
153
  let registryArg = "";
153
154
  if (registry) {
154
- // Validate and sanitize the registry URL to prevent command injection
155
+ // Validate and sanitize the registry URL to prevent command injection.
155
156
  const sanitizedRegistry = validateRegistryUrl(registry);
156
- // Quote the URL to handle any special characters safely
157
+ // Quote the URL to handle any special characters safely.
157
158
  registryArg = ` --registry "${sanitizedRegistry}"`;
158
159
  }
159
160
  if (usePnpm) {
@@ -162,19 +163,19 @@ let usePnpm = null;
162
163
  return `npm install --legacy-peer-deps --ignore-scripts${registryArg}`;
163
164
  }
164
165
  /**
165
- * Generate the entry file content based on package, subpath, and exports
166
+ * Generate the entry file content based on package, subpath, and exports.
166
167
  */ function generateEntryContent(options) {
167
168
  const { packageName, subpath, exports, allSubpaths, exportToSubpath } = options;
168
- // If we have exports mapped to different subpaths
169
+ // If we have exports mapped to different subpaths.
169
170
  if (exportToSubpath && exportToSubpath.size > 0) {
170
- // Group exports by subpath
171
+ // Group exports by subpath.
171
172
  const subpathToExports = new Map();
172
173
  for (const [exportName, sp] of exportToSubpath){
173
174
  const existing = subpathToExports.get(sp) || [];
174
175
  existing.push(exportName);
175
176
  subpathToExports.set(sp, existing);
176
177
  }
177
- // Generate imports for each subpath
178
+ // Generate imports for each subpath.
178
179
  const lines = [];
179
180
  const allExportNames = [];
180
181
  for (const [sp, exportNames] of subpathToExports){
@@ -186,43 +187,43 @@ let usePnpm = null;
186
187
  lines.push(`export { ${allExportNames.join(", ")} };`);
187
188
  return lines.join("\n") + "\n";
188
189
  }
189
- // If we have specific exports to import
190
+ // If we have specific exports to import.
190
191
  if (exports && exports.length > 0) {
191
- // Determine the import path
192
+ // Determine the import path.
192
193
  const importPath = subpath ? `${packageName}/${subpath}` : packageName;
193
194
  const importNames = exports.join(", ");
194
195
  return `import { ${importNames} } from "${importPath}";\nexport { ${importNames} };\n`;
195
196
  }
196
- // If we have a specific subpath (but no specific exports)
197
+ // If we have a specific subpath (but no specific exports).
197
198
  if (subpath) {
198
199
  const importPath = `${packageName}/${subpath}`;
199
200
  return `import * as pkg from "${importPath}";\nexport default pkg;\n`;
200
201
  }
201
- // If package has subpath exports only (no main entry), import all subpaths
202
+ // If package has subpath exports only (no main entry), import all subpaths.
202
203
  if (allSubpaths && allSubpaths.length > 0) {
203
204
  const imports = allSubpaths.map((sp, i)=>`import * as sub${i} from "${packageName}/${sp}";\nexport { sub${i} };`).join("\n");
204
205
  return imports + "\n";
205
206
  }
206
- // Default: import everything from main entry
207
+ // Default: import everything from main entry.
207
208
  return `import * as pkg from "${packageName}";\nexport default pkg;\n`;
208
209
  }
209
210
  /**
210
- * Get externals list based on options
211
+ * Get externals list based on options.
211
212
  */ export function getExternals(packageName, externals, noExternal) {
212
213
  if (noExternal) {
213
214
  return [];
214
215
  }
215
- // Start with default externals (react, react-dom)
216
+ // Start with default externals (react, react-dom).
216
217
  let result = [
217
218
  ...DEFAULT_EXTERNALS
218
219
  ];
219
- // If checking react or react-dom themselves, don't mark them as external
220
+ // If checking react or react-dom themselves, don't mark them as external.
220
221
  if (packageName === "react") {
221
222
  result = result.filter((e)=>e !== "react");
222
223
  } else if (packageName === "react-dom") {
223
224
  result = result.filter((e)=>e !== "react-dom");
224
225
  }
225
- // Add any additional externals
226
+ // Add any additional externals.
226
227
  if (externals && externals.length > 0) {
227
228
  result = [
228
229
  ...new Set([
@@ -234,14 +235,15 @@ let usePnpm = null;
234
235
  return result;
235
236
  }
236
237
  /**
237
- * Get version, dependencies, peer dependencies, and exports from an installed package
238
+ * Get version, dependencies, peer dependencies, and exports from an installed
239
+ * package.
238
240
  */ function getPackageInfo(tmpDir, packageName) {
239
241
  try {
240
- // Handle scoped packages - the package name in node_modules
242
+ // Handle scoped packages - the package name in node_modules.
241
243
  const pkgJsonPath = path.join(tmpDir, "node_modules", packageName, "package.json");
242
244
  if (fs.existsSync(pkgJsonPath)) {
243
245
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
244
- // Check if package has a main entry point
246
+ // Check if package has a main entry point.
245
247
  const hasMainEntry = Boolean(pkgJson.main || pkgJson.module || pkgJson.exports?.["."] || pkgJson.exports?.["./index"] || !pkgJson.exports && !pkgJson.main && !pkgJson.module);
246
248
  return {
247
249
  version: pkgJson.version || "unknown",
@@ -253,7 +255,7 @@ let usePnpm = null;
253
255
  };
254
256
  }
255
257
  } catch {
256
- // Ignore errors reading package info
258
+ // Ignore errors reading package info.
257
259
  }
258
260
  return {
259
261
  version: "unknown",
@@ -265,19 +267,19 @@ let usePnpm = null;
265
267
  };
266
268
  }
267
269
  /**
268
- * Extract subpath export names from package exports field
269
- * Returns array of subpaths like ["header", "body", "datagrid"]
270
+ * Extract subpath export names from package exports field Returns array of
271
+ * subpaths like ["header", "body", "datagrid"].
270
272
  */ function getSubpathExports(exports) {
271
273
  if (!exports) {
272
274
  return [];
273
275
  }
274
276
  const subpaths = [];
275
277
  for (const key of Object.keys(exports)){
276
- // Skip the main entry point and package.json
278
+ // Skip the main entry point and package.json.
277
279
  if (key === "." || key === "./package.json") {
278
280
  continue;
279
281
  }
280
- // Remove leading "./" to get subpath name
282
+ // Remove leading "./" to get subpath name.
281
283
  if (key.startsWith("./")) {
282
284
  subpaths.push(key.substring(2));
283
285
  }
@@ -285,20 +287,20 @@ let usePnpm = null;
285
287
  return subpaths;
286
288
  }
287
289
  /**
288
- * Find which subpath(s) export the given component names
289
- * Reads type definition files or JS files to find the exports
290
+ * Find which subpath(s) export the given component names Reads type definition
291
+ * files or JS files to find the exports.
290
292
  */ function findSubpathsForExports(tmpDir, packageName, exports, componentNames) {
291
293
  const packageDir = path.join(tmpDir, "node_modules", packageName);
292
294
  const exportToSubpath = new Map();
293
295
  for (const [subpathKey, subpathValue] of Object.entries(exports)){
294
- // Skip main entry and package.json
296
+ // Skip main entry and package.json.
295
297
  if (subpathKey === "." || subpathKey === "./package.json") {
296
298
  continue;
297
299
  }
298
- // Get the types or import path
300
+ // Get the types or import path.
299
301
  let filePath;
300
302
  if (typeof subpathValue === "object" && subpathValue !== null) {
301
- // Prefer types file for more accurate export detection
303
+ // Prefer types file for more accurate export detection.
302
304
  filePath = subpathValue.types || subpathValue.import;
303
305
  } else if (typeof subpathValue === "string") {
304
306
  filePath = subpathValue;
@@ -306,21 +308,21 @@ let usePnpm = null;
306
308
  if (!filePath) {
307
309
  continue;
308
310
  }
309
- // Resolve the file path
311
+ // Resolve the file path.
310
312
  const fullPath = path.join(packageDir, filePath);
311
313
  try {
312
314
  if (fs.existsSync(fullPath)) {
313
315
  const content = fs.readFileSync(fullPath, "utf-8");
314
316
  const subpath = subpathKey.startsWith("./") ? subpathKey.substring(2) : subpathKey;
315
- // Check each component name
317
+ // Check each component name.
316
318
  for (const name of componentNames){
317
- // Skip if already found
319
+ // Skip if already found.
318
320
  if (exportToSubpath.has(name)) {
319
321
  continue;
320
322
  }
321
- // Escape regex special characters in the name to prevent injection
323
+ // Escape regex special characters in the name to prevent injection.
322
324
  const escapedName = escapeRegExp(name);
323
- // Look for various export patterns
325
+ // Look for various export patterns.
324
326
  const patterns = [
325
327
  new RegExp(`export\\s*\\{[^}]*\\b${escapedName}\\b[^}]*\\}`, "m"),
326
328
  new RegExp(`export\\s+declare\\s+(?:const|function|class)\\s+${escapedName}\\b`, "m"),
@@ -332,14 +334,14 @@ let usePnpm = null;
332
334
  }
333
335
  }
334
336
  } catch {
335
- // Ignore read errors, continue to next subpath
337
+ // Ignore read errors, continue to next subpath.
336
338
  }
337
339
  }
338
- // Check if all exports were found
340
+ // Check if all exports were found.
339
341
  if (exportToSubpath.size !== componentNames.length) {
340
342
  return {}; // Not all exports found
341
343
  }
342
- // Check if all exports are from the same subpath
344
+ // Check if all exports are from the same subpath.
343
345
  const subpaths = new Set(exportToSubpath.values());
344
346
  if (subpaths.size === 1) {
345
347
  return {
@@ -348,20 +350,20 @@ let usePnpm = null;
348
350
  ][0]
349
351
  };
350
352
  }
351
- // Multiple subpaths needed
353
+ // Multiple subpaths needed.
352
354
  return {
353
355
  exportToSubpath
354
356
  };
355
357
  }
356
358
  /**
357
- * Check the bundle size of an npm package
359
+ * Check the bundle size of an npm package.
358
360
  */ export async function checkBundleSize(options) {
359
361
  const { packageName: packageSpecifier, exports, additionalExternals, noExternal, gzipLevel = 5, registry, platform: explicitPlatform } = options;
360
- // Parse the package specifier to extract name, version, and subpath
362
+ // Parse the package specifier to extract name, version, and subpath.
361
363
  const { name: packageName, version: requestedVersion, subpath } = parsePackageSpecifier(packageSpecifier);
362
364
  const tmpDir = createTempDir();
363
365
  try {
364
- // Create initial package.json
366
+ // Create initial package.json.
365
367
  const packageJson = {
366
368
  name: "bundlecheck-temp",
367
369
  version: "1.0.0",
@@ -371,24 +373,28 @@ let usePnpm = null;
371
373
  }
372
374
  };
373
375
  fs.writeFileSync(path.join(tmpDir, "package.json"), JSON.stringify(packageJson, null, 2));
374
- // Install the main package (try pnpm first, fallback to npm)
376
+ // Install the main package (try pnpm first, fallback to npm).
375
377
  const installCmd = getInstallCommand(registry);
376
378
  execSync(installCmd, {
377
379
  cwd: tmpDir,
378
380
  stdio: "pipe"
379
381
  });
380
- // Get package info (version, dependencies, peer dependencies, exports, engines)
381
- const pkgInfo = getPackageInfo(tmpDir, packageName);
382
+ /**
383
+ * Get package info (version, dependencies, peer dependencies, exports,
384
+ * engines).
385
+ */ const pkgInfo = getPackageInfo(tmpDir, packageName);
382
386
  const peerDepKeys = Object.keys(pkgInfo.peerDependencies);
383
- // Determine platform: use explicit value if provided, otherwise auto-detect from engines
384
- let platform = "browser";
387
+ /**
388
+ * Determine platform: use explicit value if provided, otherwise auto-detect
389
+ * from engines.
390
+ */ let platform = "browser";
385
391
  if (explicitPlatform) {
386
392
  platform = explicitPlatform;
387
393
  } else if (pkgInfo.engines?.node && !pkgInfo.engines?.browser) {
388
- // Package specifies node engine without browser - likely a Node.js package
394
+ // Package specifies node engine without browser - likely a Node.js package.
389
395
  platform = "node";
390
396
  }
391
- // Collect all dependency names (prod + peer)
397
+ // Collect all dependency names (prod + peer).
392
398
  const allDependencies = [
393
399
  ...new Set([
394
400
  ...Object.keys(pkgInfo.dependencies),
@@ -396,40 +402,42 @@ let usePnpm = null;
396
402
  ])
397
403
  ].sort();
398
404
  if (peerDepKeys.length > 0) {
399
- // Add peer dependencies to package.json
405
+ // Add peer dependencies to package.json.
400
406
  for (const dep of peerDepKeys){
401
- // Use the version range from peer dependencies
407
+ // Use the version range from peer dependencies.
402
408
  packageJson.dependencies[dep] = pkgInfo.peerDependencies[dep];
403
409
  }
404
- // Update package.json and reinstall
410
+ // Update package.json and reinstall.
405
411
  fs.writeFileSync(path.join(tmpDir, "package.json"), JSON.stringify(packageJson, null, 2));
406
412
  execSync(installCmd, {
407
413
  cwd: tmpDir,
408
414
  stdio: "pipe"
409
415
  });
410
416
  }
411
- // Determine if we need to use all subpath exports or find the right subpath(s)
412
- let allSubpaths;
417
+ /**
418
+ * Determine if we need to use all subpath exports or find the right
419
+ * subpath(s).
420
+ */ let allSubpaths;
413
421
  let resolvedSubpath = subpath;
414
422
  let exportToSubpath;
415
423
  if (!subpath && !pkgInfo.hasMainEntry && pkgInfo.exports) {
416
424
  if (exports && exports.length > 0) {
417
- // User specified exports but no subpath - try to find the right subpath(s)
425
+ // User specified exports but no subpath - try to find the right subpath(s).
418
426
  const mapping = findSubpathsForExports(tmpDir, packageName, pkgInfo.exports, exports);
419
427
  if (mapping.singleSubpath) {
420
- // All exports from the same subpath
428
+ // All exports from the same subpath.
421
429
  resolvedSubpath = mapping.singleSubpath;
422
430
  } else if (mapping.exportToSubpath) {
423
- // Exports from multiple subpaths
431
+ // Exports from multiple subpaths.
424
432
  exportToSubpath = mapping.exportToSubpath;
425
433
  }
426
434
  }
427
- // If still no subpath resolved and no mapping, bundle all subpaths
435
+ // If still no subpath resolved and no mapping, bundle all subpaths.
428
436
  if (!resolvedSubpath && !exportToSubpath) {
429
437
  allSubpaths = getSubpathExports(pkgInfo.exports);
430
438
  }
431
439
  }
432
- // Create entry file with appropriate content
440
+ // Create entry file with appropriate content.
433
441
  const entryContent = generateEntryContent({
434
442
  packageName,
435
443
  subpath: resolvedSubpath,
@@ -439,9 +447,9 @@ let usePnpm = null;
439
447
  });
440
448
  const entryFile = path.join(tmpDir, "entry.js");
441
449
  fs.writeFileSync(entryFile, entryContent);
442
- // Get externals
450
+ // Get externals.
443
451
  const externals = getExternals(packageName, additionalExternals, noExternal);
444
- // Bundle with esbuild
452
+ // Bundle with esbuild.
445
453
  const result = await esbuild.build({
446
454
  entryPoints: [
447
455
  entryFile
@@ -456,10 +464,10 @@ let usePnpm = null;
456
464
  external: externals,
457
465
  metafile: true
458
466
  });
459
- // Get raw size
467
+ // Get raw size.
460
468
  const bundleContent = result.outputFiles[0].contents;
461
469
  const rawSize = bundleContent.length;
462
- // Gzip the bundle (only for browser platform - not relevant for Node.js)
470
+ // Gzip the bundle (only for browser platform - not relevant for Node.js).
463
471
  let gzipSize = null;
464
472
  if (platform === "browser") {
465
473
  const gzipped = await gzipAsync(Buffer.from(bundleContent), {
@@ -467,12 +475,12 @@ let usePnpm = null;
467
475
  });
468
476
  gzipSize = gzipped.length;
469
477
  }
470
- // Determine the display name
478
+ // Determine the display name.
471
479
  let displayName = packageName;
472
480
  if (resolvedSubpath) {
473
481
  displayName = `${packageName}/${resolvedSubpath}`;
474
482
  } else if (exportToSubpath && exportToSubpath.size > 0) {
475
- // Multiple subpaths - show them all
483
+ // Multiple subpaths - show them all.
476
484
  const uniqueSubpaths = [
477
485
  ...new Set(exportToSubpath.values())
478
486
  ].sort();