Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions packages/bundlecheck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A CLI tool to check the bundle size of npm packages, similar to [bundlephobia.co
- Support for checking specific exports (tree-shaking)
- Automatic externalization of React and React-DOM
- Raw and gzip sizes with configurable compression level
- **Platform support**: target `browser` (default) or `node` with smart auto-detection
- Custom npm registry support (for private registries)
- Fast bundling using esbuild (with pnpm support)

Expand Down Expand Up @@ -92,17 +93,18 @@ bundlecheck lodash "debounce,throttle"

### Options

| Flag | Short | Description |
| ------------------- | ----------- | ------------------------------------------------------- |
| `--help` | `-h` | Display help instructions |
| `--version` | `-v` | Output the current version |
| `--versions` | `-V` | Choose from available package versions interactively |
| `--trend [N]` | `-t [N]` | Show bundle size trend for N versions (default: 5) |
| `--boring` | `-b` | Do not use color output |
| `--gzipLevel <n>` | `-g <n>` | Gzip compression level (1-9, default: 5) |
| `--external <pkgs>` | `-e <pkgs>` | Comma-separated additional packages to mark as external |
| `--noExternal` | `-n` | Do not mark any packages as external |
| `--registry <url>` | `-r <url>` | Custom npm registry URL (default: registry.npmjs.org) |
| Flag | Short | Description |
| ------------------- | ----------- | ---------------------------------------------------------------- |
| `--help` | `-h` | Display help instructions |
| `--version` | `-v` | Output the current version |
| `--versions` | `-V` | Choose from available package versions interactively |
| `--trend [N]` | `-t [N]` | Show bundle size trend for N versions (default: 5) |
| `--boring` | `-b` | Do not use color output |
| `--gzipLevel <n>` | `-g <n>` | Gzip compression level (1-9, default: 5) |
| `--external <pkgs>` | `-e <pkgs>` | Comma-separated additional packages to mark as external |
| `--noExternal` | `-n` | Do not mark any packages as external |
| `--registry <url>` | `-r <url>` | Custom npm registry URL (default: registry.npmjs.org) |
| `--platform <name>` | `-p <name>` | Target platform: `auto` (default), `browser`, or `node` |

### Examples

Expand Down Expand Up @@ -137,6 +139,12 @@ bundlecheck lodash --trend 3

# Use a custom npm registry
bundlecheck @myorg/private-pkg --registry https://npm.mycompany.com

# Check a Node.js package (explicit platform)
bundlecheck express --platform node

# Auto-detect platform (default behavior)
bundlecheck express # auto-detects "node" from package.json engines
```

## How It Works
Expand All @@ -148,6 +156,27 @@ bundlecheck @myorg/private-pkg --registry https://npm.mycompany.com
5. Reports raw and gzip sizes
6. Cleans up temporary files

## Platform Support

The `--platform` flag controls how the bundle is built:

- **`auto`** (default): Automatically detects the platform from the package's `engines` field. If the package specifies `engines.node` without `engines.browser`, it uses `node`; otherwise defaults to `browser`.
- **`browser`**: Builds for browser environments (also accepts aliases: `web`, `desktop`, `client`)
- **`node`**: Builds for Node.js environments (also accepts aliases: `server`, `nodejs`, `backend`)

When targeting **node** platform:
- Gzip size is not calculated (shows "N/A") since server-side code isn't typically served compressed over HTTP
- The bundle is optimized for Node.js built-ins

```bash
# Auto-detect (recommended for most cases)
bundlecheck express # detects "node" from engines.node

# Explicit platform
bundlecheck lodash --platform browser
bundlecheck fastify -p server # "server" is an alias for "node"
```

## Default Externals

By default, `react` and `react-dom` are marked as external (not included in the bundle size) since most React-based packages expect these as peer dependencies. This matches how these packages would typically be used in a real application.
Expand Down
146 changes: 146 additions & 0 deletions packages/bundlecheck/src/__tests__/bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,4 +893,150 @@ describe("checkBundleSize", () => {

expect(result).toBeDefined();
});

describe("platform option", () => {
it("should default to browser platform", async () => {
const result = await checkBundleSize({
packageName: "test-package",
});

expect(result.platform).toBe("browser");
});

it("should use explicit browser platform", async () => {
const result = await checkBundleSize({
packageName: "test-package",
platform: "browser",
});

expect(result.platform).toBe("browser");
expect(result.gzipSize).not.toBeNull();
});

it("should use explicit node platform", async () => {
const result = await checkBundleSize({
packageName: "test-package",
platform: "node",
});

expect(result.platform).toBe("node");
});

it("should return null gzipSize for node platform", async () => {
const result = await checkBundleSize({
packageName: "test-package",
platform: "node",
});

expect(result.gzipSize).toBeNull();
});

it("should return gzipSize for browser platform", async () => {
const result = await checkBundleSize({
packageName: "test-package",
platform: "browser",
});

expect(result.gzipSize).not.toBeNull();
expect(result.gzipSize).toBeGreaterThan(0);
});

it("should auto-detect node platform from engines.node", async () => {
vi.mocked(fs.readFileSync).mockReturnValue(
JSON.stringify({
name: "test-package",
version: "1.0.0",
main: "./dist/index.js",
dependencies: {},
peerDependencies: {},
engines: {
node: ">=18.0.0",
},
}),
);

const result = await checkBundleSize({
packageName: "test-package",
});

expect(result.platform).toBe("node");
expect(result.gzipSize).toBeNull();
});

it("should default to browser when no engines specified", async () => {
vi.mocked(fs.readFileSync).mockReturnValue(
JSON.stringify({
name: "test-package",
version: "1.0.0",
main: "./dist/index.js",
dependencies: {},
peerDependencies: {},
}),
);

const result = await checkBundleSize({
packageName: "test-package",
});

expect(result.platform).toBe("browser");
});

it("should use explicit platform even when engines.node is present", async () => {
vi.mocked(fs.readFileSync).mockReturnValue(
JSON.stringify({
name: "test-package",
version: "1.0.0",
main: "./dist/index.js",
dependencies: {},
peerDependencies: {},
engines: {
node: ">=18.0.0",
},
}),
);

const result = await checkBundleSize({
packageName: "test-package",
platform: "browser",
});

expect(result.platform).toBe("browser");
expect(result.gzipSize).not.toBeNull();
});

it("should pass platform to esbuild", async () => {
await checkBundleSize({
packageName: "test-package",
platform: "node",
});

expect(esbuild.build).toHaveBeenCalledWith(
expect.objectContaining({
platform: "node",
}),
);
});

it("should not auto-detect node when engines.browser is also present", async () => {
vi.mocked(fs.readFileSync).mockReturnValue(
JSON.stringify({
name: "test-package",
version: "1.0.0",
main: "./dist/index.js",
dependencies: {},
peerDependencies: {},
engines: {
node: ">=18.0.0",
browser: ">= ES2020",
},
}),
);

const result = await checkBundleSize({
packageName: "test-package",
});

expect(result.platform).toBe("browser");
});
});
});
140 changes: 140 additions & 0 deletions packages/bundlecheck/src/__tests__/defaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { describe, expect, it } from "vitest";
import { isValidPlatform, normalizePlatform } from "../defaults.js";

describe("normalizePlatform", () => {
describe("auto detection", () => {
it("should return undefined for 'auto'", () => {
expect(normalizePlatform("auto")).toBeUndefined();
});

it("should return undefined for 'AUTO' (case insensitive)", () => {
expect(normalizePlatform("AUTO")).toBeUndefined();
});

it("should return undefined for undefined input", () => {
expect(normalizePlatform(undefined)).toBeUndefined();
});
});

describe("browser aliases", () => {
it("should normalize 'browser' to 'browser'", () => {
expect(normalizePlatform("browser")).toBe("browser");
});

it("should normalize 'web' to 'browser'", () => {
expect(normalizePlatform("web")).toBe("browser");
});

it("should normalize 'desktop' to 'browser'", () => {
expect(normalizePlatform("desktop")).toBe("browser");
});

it("should normalize 'client' to 'browser'", () => {
expect(normalizePlatform("client")).toBe("browser");
});

it("should handle case insensitivity for browser aliases", () => {
expect(normalizePlatform("BROWSER")).toBe("browser");
expect(normalizePlatform("Web")).toBe("browser");
expect(normalizePlatform("DESKTOP")).toBe("browser");
expect(normalizePlatform("CLIENT")).toBe("browser");
});

it("should handle whitespace for browser aliases", () => {
expect(normalizePlatform(" browser ")).toBe("browser");
expect(normalizePlatform(" web ")).toBe("browser");
});
});

describe("node aliases", () => {
it("should normalize 'node' to 'node'", () => {
expect(normalizePlatform("node")).toBe("node");
});

it("should normalize 'server' to 'node'", () => {
expect(normalizePlatform("server")).toBe("node");
});

it("should normalize 'nodejs' to 'node'", () => {
expect(normalizePlatform("nodejs")).toBe("node");
});

it("should normalize 'backend' to 'node'", () => {
expect(normalizePlatform("backend")).toBe("node");
});

it("should handle case insensitivity for node aliases", () => {
expect(normalizePlatform("NODE")).toBe("node");
expect(normalizePlatform("Server")).toBe("node");
expect(normalizePlatform("NODEJS")).toBe("node");
expect(normalizePlatform("Backend")).toBe("node");
});

it("should handle whitespace for node aliases", () => {
expect(normalizePlatform(" node ")).toBe("node");
expect(normalizePlatform(" server ")).toBe("node");
});
});

describe("invalid values", () => {
it("should return the invalid value as-is (to be caught by validation)", () => {
expect(normalizePlatform("invalid")).toBe("invalid");
expect(normalizePlatform("unknown")).toBe("unknown");
});
});
});

describe("isValidPlatform", () => {
describe("valid values", () => {
it("should accept undefined", () => {
expect(isValidPlatform(undefined)).toBe(true);
});

it("should accept 'auto'", () => {
expect(isValidPlatform("auto")).toBe(true);
});

it("should accept browser aliases", () => {
expect(isValidPlatform("browser")).toBe(true);
expect(isValidPlatform("web")).toBe(true);
expect(isValidPlatform("desktop")).toBe(true);
expect(isValidPlatform("client")).toBe(true);
});

it("should accept node aliases", () => {
expect(isValidPlatform("node")).toBe(true);
expect(isValidPlatform("server")).toBe(true);
expect(isValidPlatform("nodejs")).toBe(true);
expect(isValidPlatform("backend")).toBe(true);
});

it("should be case insensitive", () => {
expect(isValidPlatform("BROWSER")).toBe(true);
expect(isValidPlatform("Node")).toBe(true);
expect(isValidPlatform("AUTO")).toBe(true);
});

it("should handle whitespace", () => {
expect(isValidPlatform(" browser ")).toBe(true);
expect(isValidPlatform(" node ")).toBe(true);
});
});

describe("invalid values", () => {
it("should reject invalid platform names", () => {
expect(isValidPlatform("invalid")).toBe(false);
expect(isValidPlatform("neutral")).toBe(false);
expect(isValidPlatform("windows")).toBe(false);
expect(isValidPlatform("linux")).toBe(false);
});

it("should reject empty string", () => {
expect(isValidPlatform("")).toBe(false);
});

it("should reject misspellings", () => {
expect(isValidPlatform("browsr")).toBe(false); // cSpell:ignore browsr
expect(isValidPlatform("nod")).toBe(false);
});
});
});
Loading
Loading