From 1ec805cd79a72cba5dab1eb12835e63664de41c8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 21:08:54 +0000 Subject: [PATCH] feat(bundlecheck): update default esbuild target to es2022 and add target option - Changed default esbuild target from es2020 to es2022 - Added new 'target' option for both CLI (--target/-T) and API - Added target validation (es2015-es2024, esnext) - Updated README with new target option documentation - Added unit tests for target normalization and validation --- packages/bundlecheck/README.md | 6 ++ .../src/__tests__/defaults.test.ts | 85 ++++++++++++++++++- packages/bundlecheck/src/bundlecheck.ts | 11 ++- packages/bundlecheck/src/bundler.ts | 15 +++- packages/bundlecheck/src/defaults.ts | 52 ++++++++++++ packages/bundlecheck/src/index.ts | 24 +++++- packages/bundlecheck/src/parse.ts | 20 ++++- packages/bundlecheck/src/trend.ts | 6 ++ 8 files changed, 212 insertions(+), 7 deletions(-) diff --git a/packages/bundlecheck/README.md b/packages/bundlecheck/README.md index c1ed8dbd..9021c376 100644 --- a/packages/bundlecheck/README.md +++ b/packages/bundlecheck/README.md @@ -108,6 +108,7 @@ bundlecheck lodash "debounce,throttle" | `--registry ` | `-r ` | Custom npm registry URL (default: registry.npmjs.org) | | `--platform ` | `-p ` | Target platform: `auto` (default), `browser`, or `node` | | `--force` | `-f` | Bypass cache and force re-fetch/re-calculation | +| `--target ` | `-T ` | esbuild target (e.g., "es2022", "es2020"). Default: "es2022" | ### Examples @@ -151,6 +152,9 @@ bundlecheck express # auto-detects "node" from package.json engines # Bypass cache and force re-fetch bundlecheck lodash --force + +# Use a specific esbuild target (default: es2022) +bundlecheck lodash --target es2020 ``` ## Programmatic Usage (Library API) @@ -253,6 +257,7 @@ Get bundle size statistics for a single package. | `registry` | `string` | `undefined`| Custom npm registry URL | | `platform` | `"browser" \| "node" \| "auto"` | `"auto"` | Target platform | | `force` | `boolean` | `false` | Bypass cache | +| `target` | `string` | `"es2022"` | esbuild target (e.g., "es2022", "es2020") | **Returns:** `Promise` @@ -291,6 +296,7 @@ Get bundle size trend across multiple versions. | `registry` | `string` | `undefined`| Custom npm registry URL | | `platform` | `"browser" \| "node" \| "auto"` | `"auto"` | Target platform | | `force` | `boolean` | `false` | Bypass cache | +| `target` | `string` | `"es2022"` | esbuild target (e.g., "es2022", "es2020") | **Returns:** `Promise` diff --git a/packages/bundlecheck/src/__tests__/defaults.test.ts b/packages/bundlecheck/src/__tests__/defaults.test.ts index 81ea66da..ef31a3ea 100644 --- a/packages/bundlecheck/src/__tests__/defaults.test.ts +++ b/packages/bundlecheck/src/__tests__/defaults.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from "vitest"; -import { isValidPlatform, normalizePlatform } from "../defaults.js"; +import { + DEFAULT_TARGET, + isValidPlatform, + isValidTarget, + normalizePlatform, + normalizeTarget, +} from "../defaults.js"; describe("normalizePlatform", () => { describe("auto detection", () => { @@ -138,3 +144,80 @@ describe("isValidPlatform", () => { }); }); }); + +describe("normalizeTarget", () => { + describe("default value", () => { + it("should return DEFAULT_TARGET for undefined input", () => { + expect(normalizeTarget(undefined)).toBe(DEFAULT_TARGET); + }); + + it("should return DEFAULT_TARGET for empty string", () => { + expect(normalizeTarget("")).toBe(DEFAULT_TARGET); + }); + }); + + describe("normalization", () => { + it("should normalize to lowercase", () => { + expect(normalizeTarget("ES2022")).toBe("es2022"); + expect(normalizeTarget("ES2020")).toBe("es2020"); + expect(normalizeTarget("ESNext")).toBe("esnext"); + }); + + it("should trim whitespace", () => { + expect(normalizeTarget(" es2022 ")).toBe("es2022"); + expect(normalizeTarget(" es2020 ")).toBe("es2020"); + }); + }); +}); + +describe("isValidTarget", () => { + describe("valid values", () => { + it("should accept undefined", () => { + expect(isValidTarget(undefined)).toBe(true); + }); + + it("should accept ECMAScript versions", () => { + expect(isValidTarget("es2015")).toBe(true); + expect(isValidTarget("es2016")).toBe(true); + expect(isValidTarget("es2017")).toBe(true); + expect(isValidTarget("es2018")).toBe(true); + expect(isValidTarget("es2019")).toBe(true); + expect(isValidTarget("es2020")).toBe(true); + expect(isValidTarget("es2021")).toBe(true); + expect(isValidTarget("es2022")).toBe(true); + expect(isValidTarget("es2023")).toBe(true); + expect(isValidTarget("es2024")).toBe(true); + expect(isValidTarget("esnext")).toBe(true); + }); + + it("should be case insensitive", () => { + expect(isValidTarget("ES2022")).toBe(true); + expect(isValidTarget("ES2020")).toBe(true); + expect(isValidTarget("ESNEXT")).toBe(true); + }); + + it("should handle whitespace", () => { + expect(isValidTarget(" es2022 ")).toBe(true); + expect(isValidTarget(" es2020 ")).toBe(true); + }); + }); + + describe("invalid values", () => { + it("should reject invalid target names", () => { + expect(isValidTarget("invalid")).toBe(false); + expect(isValidTarget("es5")).toBe(false); + expect(isValidTarget("es6")).toBe(false); + expect(isValidTarget("es2014")).toBe(false); + expect(isValidTarget("es2025")).toBe(false); + }); + + it("should reject empty string", () => { + expect(isValidTarget("")).toBe(false); + }); + + it("should reject misspellings", () => { + expect(isValidTarget("es202")).toBe(false); + expect(isValidTarget("esnex")).toBe(false); + }); + }); +}); diff --git a/packages/bundlecheck/src/bundlecheck.ts b/packages/bundlecheck/src/bundlecheck.ts index a4d6d784..390e0ee7 100644 --- a/packages/bundlecheck/src/bundlecheck.ts +++ b/packages/bundlecheck/src/bundlecheck.ts @@ -15,7 +15,11 @@ import { normalizeCacheKey, setCachedResult, } from "./cache.js"; -import { normalizePlatform, TREND_VERSION_COUNT } from "./defaults.js"; +import { + normalizePlatform, + normalizeTarget, + TREND_VERSION_COUNT, +} from "./defaults.js"; import { config } from "./parse.js"; import { analyzeTrend, @@ -124,6 +128,9 @@ async function main() { // Normalize platform from flag (handles aliases like "web" → "browser"). const platform = normalizePlatform(flags?.platform); + // Normalize target (e.g., lowercase). + const target = normalizeTarget(flags?.target); + /** * If --trend flag is set, show bundle size trend across versions --trend alone * uses default (5), --trend N uses N versions. @@ -172,6 +179,7 @@ async function main() { registry: flags?.registry, platform, force: flags?.force, + target, }); if (results.length === 0) { @@ -289,6 +297,7 @@ async function main() { gzipLevel: flags?.gzipLevel, registry: flags?.registry, platform, + target, }); // Store result in cache. diff --git a/packages/bundlecheck/src/bundler.ts b/packages/bundlecheck/src/bundler.ts index 7005d277..7b6edabf 100644 --- a/packages/bundlecheck/src/bundler.ts +++ b/packages/bundlecheck/src/bundler.ts @@ -5,7 +5,11 @@ import path from "node:path"; import { promisify } from "node:util"; import zlib from "node:zlib"; import * as esbuild from "esbuild"; -import { DEFAULT_EXTERNALS, EXTERNAL_SUBPATHS } from "./defaults.js"; +import { + DEFAULT_EXTERNALS, + DEFAULT_TARGET, + EXTERNAL_SUBPATHS, +} from "./defaults.js"; import { getNamedExports } from "./exports.js"; const gzipAsync = promisify(zlib.gzip); @@ -88,6 +92,10 @@ export type BundleOptions = { * Target platform. If undefined, auto-detects from package.json engines. */ platform?: "browser" | "node"; + /** + * esbuild target (e.g., "es2022", "es2020"). Defaults to "es2022". + */ + target?: string; }; export type BundleResult = { @@ -617,6 +625,7 @@ export async function checkBundleSize( gzipLevel = 5, registry, platform: explicitPlatform, + target = DEFAULT_TARGET, } = options; // Parse the package specifier to extract name, version, and subpath. @@ -775,7 +784,7 @@ export async function checkBundleSize( write: false, format: "esm", platform, - target: "es2020", + target, minify: true, treeShaking: true, external: esbuildExternals, @@ -811,7 +820,7 @@ export async function checkBundleSize( write: false, format: "esm", platform, - target: "es2020", + target, minify: true, treeShaking: true, external: esbuildExternals, diff --git a/packages/bundlecheck/src/defaults.ts b/packages/bundlecheck/src/defaults.ts index 7d4eb04c..c8654e39 100644 --- a/packages/bundlecheck/src/defaults.ts +++ b/packages/bundlecheck/src/defaults.ts @@ -9,6 +9,7 @@ export const defaultFlags = { registry: "", platform: "auto", force: false, + target: "es2022", }; /** @@ -79,6 +80,57 @@ export function isValidPlatform(platform: string | undefined): boolean { export const TREND_VERSION_COUNT = 5; +/** + * Default esbuild target for bundling. + */ +export const DEFAULT_TARGET = "es2022"; + +/** + * Valid esbuild target values. + * Includes ECMAScript versions (es2015-es2024, esnext) and browser/runtime targets. + */ +export const VALID_TARGETS = [ + "es2015", + "es2016", + "es2017", + "es2018", + "es2019", + "es2020", + "es2021", + "es2022", + "es2023", + "es2024", + "esnext", +]; + +/** + * Check if a target value is valid. + */ +export function isValidTarget(target: string | undefined): boolean { + if (target === undefined) { + return true; + } + + const normalized = target.toLowerCase().trim(); + + // Empty string after trim is invalid. + if (normalized === "") { + return false; + } + + return VALID_TARGETS.includes(normalized); +} + +/** + * Normalize target to lowercase. + */ +export function normalizeTarget(target: string | undefined): string { + if (!target) { + return DEFAULT_TARGET; + } + return target.toLowerCase().trim(); +} + /** * Base packages to auto-detect for externalization. These are checked against * the package's dependencies/peerDependencies. diff --git a/packages/bundlecheck/src/index.ts b/packages/bundlecheck/src/index.ts index e9078b0b..1cf48f2a 100644 --- a/packages/bundlecheck/src/index.ts +++ b/packages/bundlecheck/src/index.ts @@ -16,7 +16,11 @@ import { normalizeCacheKey, setCachedResult, } from "./cache.js"; -import { normalizePlatform, TREND_VERSION_COUNT } from "./defaults.js"; +import { + normalizePlatform, + normalizeTarget, + TREND_VERSION_COUNT, +} from "./defaults.js"; import type { NamedExport } from "./exports.js"; import { analyzeTrend, selectTrendVersions } from "./trend.js"; import { fetchPackageVersions as fetchVersions } from "./versions.js"; @@ -65,6 +69,10 @@ export type GetBundleStatsOptions = { * Bypass cache and force re-analysis. */ force?: boolean; + /** + * esbuild target (e.g., "es2022", "es2020"). Defaults to "es2022". + */ + target?: string; }; /** @@ -167,6 +175,10 @@ export type GetBundleTrendOptions = { * Bypass cache and force re-analysis. */ force?: boolean; + /** + * esbuild target (e.g., "es2022", "es2020"). Defaults to "es2022". + */ + target?: string; }; /** @@ -313,6 +325,7 @@ export async function getBundleStats( registry, platform: platformOption = "auto", force = false, + target: targetOption, } = options; // Normalize platform. @@ -320,6 +333,9 @@ export async function getBundleStats( platformOption === "auto" ? undefined : platformOption, ); + // Normalize target. + const target = normalizeTarget(targetOption); + // Parse package specifier. const { name: baseName, version: requestedVersion } = parsePackageSpecifier(packageName); @@ -367,6 +383,7 @@ export async function getBundleStats( gzipLevel, registry, platform, + target, }); // Store in cache. @@ -404,6 +421,7 @@ export async function getBundleTrend( registry, platform: platformOption = "auto", force = false, + target: targetOption, } = options; // Normalize platform. @@ -411,6 +429,9 @@ export async function getBundleTrend( platformOption === "auto" ? undefined : platformOption, ); + // Normalize target. + const target = normalizeTarget(targetOption); + // Parse package name (ignore version if provided). const { name: baseName, subpath } = parsePackageSpecifier(packageName); const fullPackagePath = subpath ? `${baseName}/${subpath}` : baseName; @@ -440,6 +461,7 @@ export async function getBundleTrend( registry, platform, force, + target, }); if (results.length === 0) { diff --git a/packages/bundlecheck/src/parse.ts b/packages/bundlecheck/src/parse.ts index 01ee2eca..161ea7b0 100644 --- a/packages/bundlecheck/src/parse.ts +++ b/packages/bundlecheck/src/parse.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ import { parser } from "@node-cli/parser"; -import { defaultFlags, isValidPlatform } from "./defaults.js"; +import { defaultFlags, isValidPlatform, isValidTarget } from "./defaults.js"; export type Flags = { boring?: boolean; @@ -14,6 +14,7 @@ export type Flags = { registry?: string; platform?: string; force?: boolean; + target?: string; }; export type Parameters = { @@ -86,6 +87,10 @@ export const config: Configuration = parser({ command: "bundlecheck lodash --force", comment: "## Bypass cache and force re-fetch/re-calculation", }, + { + command: "bundlecheck lodash --target es2020", + comment: "## Use a specific esbuild target (default: es2022)", + }, ], flags: { gzipLevel: { @@ -155,6 +160,13 @@ export const config: Configuration = parser({ description: "Bypass cache and force re-fetch/re-calculation", type: "boolean", }, + target: { + shortFlag: "T", + default: defaultFlags.target, + description: + 'esbuild target for bundling (e.g., "es2022", "es2020"). Default: "es2022"', + type: "string", + }, }, parameters: { package: { @@ -184,6 +196,12 @@ export const config: Configuration = parser({ 'Error: Invalid platform. Use "browser" (or web, desktop, client) or "node" (or server, nodejs, backend)', test: (flags) => !isValidPlatform(flags.platform), }, + { + exit: 1, + message: () => + 'Error: Invalid target. Use an ECMAScript version (e.g., "es2022", "es2020", "esnext")', + test: (flags) => !isValidTarget(flags.target), + }, ], usage: true, defaultFlags, diff --git a/packages/bundlecheck/src/trend.ts b/packages/bundlecheck/src/trend.ts index c8304ce0..d8f368bf 100644 --- a/packages/bundlecheck/src/trend.ts +++ b/packages/bundlecheck/src/trend.ts @@ -40,6 +40,10 @@ export type TrendOptions = { * Bypass cache and force re-fetch/re-calculation. */ force?: boolean; + /** + * esbuild target (e.g., "es2022", "es2020"). Defaults to "es2022". + */ + target?: string; }; /** @@ -73,6 +77,7 @@ export async function analyzeTrend( registry, platform, force, + target, } = options; const log = new Logger({ boring }); @@ -126,6 +131,7 @@ export async function analyzeTrend( gzipLevel, registry, platform, + target, }); // Store result in cache.