@node-cli/bundlecheck 1.2.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
- */ function getExternals(packageName, externals, noExternal) {
211
+ * Get externals list based on options.
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,48 +235,51 @@ 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",
248
250
  dependencies: pkgJson.dependencies || {},
249
251
  peerDependencies: pkgJson.peerDependencies || {},
250
252
  exports: pkgJson.exports || null,
251
- hasMainEntry
253
+ hasMainEntry,
254
+ engines: pkgJson.engines || null
252
255
  };
253
256
  }
254
257
  } catch {
255
- // Ignore errors reading package info
258
+ // Ignore errors reading package info.
256
259
  }
257
260
  return {
258
261
  version: "unknown",
259
262
  dependencies: {},
260
263
  peerDependencies: {},
261
264
  exports: null,
262
- hasMainEntry: true
265
+ hasMainEntry: true,
266
+ engines: null
263
267
  };
264
268
  }
265
269
  /**
266
- * Extract subpath export names from package exports field
267
- * 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"].
268
272
  */ function getSubpathExports(exports) {
269
273
  if (!exports) {
270
274
  return [];
271
275
  }
272
276
  const subpaths = [];
273
277
  for (const key of Object.keys(exports)){
274
- // Skip the main entry point and package.json
278
+ // Skip the main entry point and package.json.
275
279
  if (key === "." || key === "./package.json") {
276
280
  continue;
277
281
  }
278
- // Remove leading "./" to get subpath name
282
+ // Remove leading "./" to get subpath name.
279
283
  if (key.startsWith("./")) {
280
284
  subpaths.push(key.substring(2));
281
285
  }
@@ -283,20 +287,20 @@ let usePnpm = null;
283
287
  return subpaths;
284
288
  }
285
289
  /**
286
- * Find which subpath(s) export the given component names
287
- * 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.
288
292
  */ function findSubpathsForExports(tmpDir, packageName, exports, componentNames) {
289
293
  const packageDir = path.join(tmpDir, "node_modules", packageName);
290
294
  const exportToSubpath = new Map();
291
295
  for (const [subpathKey, subpathValue] of Object.entries(exports)){
292
- // Skip main entry and package.json
296
+ // Skip main entry and package.json.
293
297
  if (subpathKey === "." || subpathKey === "./package.json") {
294
298
  continue;
295
299
  }
296
- // Get the types or import path
300
+ // Get the types or import path.
297
301
  let filePath;
298
302
  if (typeof subpathValue === "object" && subpathValue !== null) {
299
- // Prefer types file for more accurate export detection
303
+ // Prefer types file for more accurate export detection.
300
304
  filePath = subpathValue.types || subpathValue.import;
301
305
  } else if (typeof subpathValue === "string") {
302
306
  filePath = subpathValue;
@@ -304,21 +308,21 @@ let usePnpm = null;
304
308
  if (!filePath) {
305
309
  continue;
306
310
  }
307
- // Resolve the file path
311
+ // Resolve the file path.
308
312
  const fullPath = path.join(packageDir, filePath);
309
313
  try {
310
314
  if (fs.existsSync(fullPath)) {
311
315
  const content = fs.readFileSync(fullPath, "utf-8");
312
316
  const subpath = subpathKey.startsWith("./") ? subpathKey.substring(2) : subpathKey;
313
- // Check each component name
317
+ // Check each component name.
314
318
  for (const name of componentNames){
315
- // Skip if already found
319
+ // Skip if already found.
316
320
  if (exportToSubpath.has(name)) {
317
321
  continue;
318
322
  }
319
- // Escape regex special characters in the name to prevent injection
323
+ // Escape regex special characters in the name to prevent injection.
320
324
  const escapedName = escapeRegExp(name);
321
- // Look for various export patterns
325
+ // Look for various export patterns.
322
326
  const patterns = [
323
327
  new RegExp(`export\\s*\\{[^}]*\\b${escapedName}\\b[^}]*\\}`, "m"),
324
328
  new RegExp(`export\\s+declare\\s+(?:const|function|class)\\s+${escapedName}\\b`, "m"),
@@ -330,14 +334,14 @@ let usePnpm = null;
330
334
  }
331
335
  }
332
336
  } catch {
333
- // Ignore read errors, continue to next subpath
337
+ // Ignore read errors, continue to next subpath.
334
338
  }
335
339
  }
336
- // Check if all exports were found
340
+ // Check if all exports were found.
337
341
  if (exportToSubpath.size !== componentNames.length) {
338
342
  return {}; // Not all exports found
339
343
  }
340
- // Check if all exports are from the same subpath
344
+ // Check if all exports are from the same subpath.
341
345
  const subpaths = new Set(exportToSubpath.values());
342
346
  if (subpaths.size === 1) {
343
347
  return {
@@ -346,20 +350,20 @@ let usePnpm = null;
346
350
  ][0]
347
351
  };
348
352
  }
349
- // Multiple subpaths needed
353
+ // Multiple subpaths needed.
350
354
  return {
351
355
  exportToSubpath
352
356
  };
353
357
  }
354
358
  /**
355
- * Check the bundle size of an npm package
359
+ * Check the bundle size of an npm package.
356
360
  */ export async function checkBundleSize(options) {
357
- const { packageName: packageSpecifier, exports, additionalExternals, noExternal, gzipLevel = 5, registry } = options;
358
- // Parse the package specifier to extract name, version, and subpath
361
+ const { packageName: packageSpecifier, exports, additionalExternals, noExternal, gzipLevel = 5, registry, platform: explicitPlatform } = options;
362
+ // Parse the package specifier to extract name, version, and subpath.
359
363
  const { name: packageName, version: requestedVersion, subpath } = parsePackageSpecifier(packageSpecifier);
360
364
  const tmpDir = createTempDir();
361
365
  try {
362
- // Create initial package.json
366
+ // Create initial package.json.
363
367
  const packageJson = {
364
368
  name: "bundlecheck-temp",
365
369
  version: "1.0.0",
@@ -369,16 +373,28 @@ let usePnpm = null;
369
373
  }
370
374
  };
371
375
  fs.writeFileSync(path.join(tmpDir, "package.json"), JSON.stringify(packageJson, null, 2));
372
- // Install the main package (try pnpm first, fallback to npm)
376
+ // Install the main package (try pnpm first, fallback to npm).
373
377
  const installCmd = getInstallCommand(registry);
374
378
  execSync(installCmd, {
375
379
  cwd: tmpDir,
376
380
  stdio: "pipe"
377
381
  });
378
- // Get package info (version, dependencies, peer dependencies, exports)
379
- const pkgInfo = getPackageInfo(tmpDir, packageName);
382
+ /**
383
+ * Get package info (version, dependencies, peer dependencies, exports,
384
+ * engines).
385
+ */ const pkgInfo = getPackageInfo(tmpDir, packageName);
380
386
  const peerDepKeys = Object.keys(pkgInfo.peerDependencies);
381
- // Collect all dependency names (prod + peer)
387
+ /**
388
+ * Determine platform: use explicit value if provided, otherwise auto-detect
389
+ * from engines.
390
+ */ let platform = "browser";
391
+ if (explicitPlatform) {
392
+ platform = explicitPlatform;
393
+ } else if (pkgInfo.engines?.node && !pkgInfo.engines?.browser) {
394
+ // Package specifies node engine without browser - likely a Node.js package.
395
+ platform = "node";
396
+ }
397
+ // Collect all dependency names (prod + peer).
382
398
  const allDependencies = [
383
399
  ...new Set([
384
400
  ...Object.keys(pkgInfo.dependencies),
@@ -386,40 +402,42 @@ let usePnpm = null;
386
402
  ])
387
403
  ].sort();
388
404
  if (peerDepKeys.length > 0) {
389
- // Add peer dependencies to package.json
405
+ // Add peer dependencies to package.json.
390
406
  for (const dep of peerDepKeys){
391
- // Use the version range from peer dependencies
407
+ // Use the version range from peer dependencies.
392
408
  packageJson.dependencies[dep] = pkgInfo.peerDependencies[dep];
393
409
  }
394
- // Update package.json and reinstall
410
+ // Update package.json and reinstall.
395
411
  fs.writeFileSync(path.join(tmpDir, "package.json"), JSON.stringify(packageJson, null, 2));
396
412
  execSync(installCmd, {
397
413
  cwd: tmpDir,
398
414
  stdio: "pipe"
399
415
  });
400
416
  }
401
- // Determine if we need to use all subpath exports or find the right subpath(s)
402
- let allSubpaths;
417
+ /**
418
+ * Determine if we need to use all subpath exports or find the right
419
+ * subpath(s).
420
+ */ let allSubpaths;
403
421
  let resolvedSubpath = subpath;
404
422
  let exportToSubpath;
405
423
  if (!subpath && !pkgInfo.hasMainEntry && pkgInfo.exports) {
406
424
  if (exports && exports.length > 0) {
407
- // 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).
408
426
  const mapping = findSubpathsForExports(tmpDir, packageName, pkgInfo.exports, exports);
409
427
  if (mapping.singleSubpath) {
410
- // All exports from the same subpath
428
+ // All exports from the same subpath.
411
429
  resolvedSubpath = mapping.singleSubpath;
412
430
  } else if (mapping.exportToSubpath) {
413
- // Exports from multiple subpaths
431
+ // Exports from multiple subpaths.
414
432
  exportToSubpath = mapping.exportToSubpath;
415
433
  }
416
434
  }
417
- // If still no subpath resolved and no mapping, bundle all subpaths
435
+ // If still no subpath resolved and no mapping, bundle all subpaths.
418
436
  if (!resolvedSubpath && !exportToSubpath) {
419
437
  allSubpaths = getSubpathExports(pkgInfo.exports);
420
438
  }
421
439
  }
422
- // Create entry file with appropriate content
440
+ // Create entry file with appropriate content.
423
441
  const entryContent = generateEntryContent({
424
442
  packageName,
425
443
  subpath: resolvedSubpath,
@@ -429,9 +447,9 @@ let usePnpm = null;
429
447
  });
430
448
  const entryFile = path.join(tmpDir, "entry.js");
431
449
  fs.writeFileSync(entryFile, entryContent);
432
- // Get externals
450
+ // Get externals.
433
451
  const externals = getExternals(packageName, additionalExternals, noExternal);
434
- // Bundle with esbuild
452
+ // Bundle with esbuild.
435
453
  const result = await esbuild.build({
436
454
  entryPoints: [
437
455
  entryFile
@@ -439,27 +457,30 @@ let usePnpm = null;
439
457
  bundle: true,
440
458
  write: false,
441
459
  format: "esm",
442
- platform: "browser",
460
+ platform,
443
461
  target: "es2020",
444
462
  minify: true,
445
463
  treeShaking: true,
446
464
  external: externals,
447
465
  metafile: true
448
466
  });
449
- // Get raw size
467
+ // Get raw size.
450
468
  const bundleContent = result.outputFiles[0].contents;
451
469
  const rawSize = bundleContent.length;
452
- // Gzip the bundle
453
- const gzipped = await gzipAsync(Buffer.from(bundleContent), {
454
- level: gzipLevel
455
- });
456
- const gzipSize = gzipped.length;
457
- // Determine the display name
470
+ // Gzip the bundle (only for browser platform - not relevant for Node.js).
471
+ let gzipSize = null;
472
+ if (platform === "browser") {
473
+ const gzipped = await gzipAsync(Buffer.from(bundleContent), {
474
+ level: gzipLevel
475
+ });
476
+ gzipSize = gzipped.length;
477
+ }
478
+ // Determine the display name.
458
479
  let displayName = packageName;
459
480
  if (resolvedSubpath) {
460
481
  displayName = `${packageName}/${resolvedSubpath}`;
461
482
  } else if (exportToSubpath && exportToSubpath.size > 0) {
462
- // Multiple subpaths - show them all
483
+ // Multiple subpaths - show them all.
463
484
  const uniqueSubpaths = [
464
485
  ...new Set(exportToSubpath.values())
465
486
  ].sort();
@@ -473,7 +494,8 @@ let usePnpm = null;
473
494
  gzipSize,
474
495
  gzipLevel,
475
496
  externals,
476
- dependencies: allDependencies
497
+ dependencies: allDependencies,
498
+ platform
477
499
  };
478
500
  } finally{
479
501
  cleanupTempDir(tmpDir);