Skip to content
Draft
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
168 changes: 168 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Microbundle is a zero-configuration bundler for tiny JavaScript libraries, powered by Rollup. It takes source code and produces multiple output formats (modern ES modules, CommonJS, UMD) with minimal setup.

## Development Commands

### Building

- `npm run build` - Full build: first builds with Babel, then self-builds
- `npm run build:babel` - Build using Babel only
- `npm run build:self` - Self-build using microbundle's own dist output
- `npm run prepare` - Runs full build (called before npm publish)

### Testing

- `npm test` - Run linter, build, and Jest tests
- `npm run jest` - Run Jest tests only (without lint/build)
- Test timeout is set to 11 seconds for fixture tests

### Linting and Formatting

- `npm run lint` - Run ESLint on src directory
- `npm run format` - Format all JS and CSS files with Prettier

### Running Tests

Individual tests can be run with Jest's standard CLI options:

```bash
npm run jest -- --testNamePattern="build shebang"
npm run jest -- test/index.test.js
```

## Architecture

### Entry Points

- **src/cli.js** - CLI entry point (shebang script), parses arguments and invokes main function
- **src/index.js** - Main bundling logic, exports the `microbundle()` function
- **src/prog.js** - CLI command definitions using `sade` library

### Core Build Flow

1. **Input Resolution** (`getInput()` in src/index.js:205)

- Resolves entry files from CLI args, `source` field in package.json, or defaults (src/index.js, index.js)
- Supports glob patterns for multiple entries
- Handles TypeScript (.ts/.tsx) and JavaScript files

2. **Output Resolution** (`getOutput()` in src/index.js:227)

- Determines output location from CLI args or package.json `main` field
- Defaults to dist/ directory

3. **Format Generation** (`getMain()` in src/index.js:278)

- Maps each format (modern, es, cjs, umd) to appropriate output filenames
- Reads from package.json fields: `module`, `main`, `exports`, `unpkg`, etc.
- Respects `{"type":"module"}` for ES Module packages

4. **Configuration Creation** (`createConfig()` in src/index.js:327)

- Creates Rollup configuration for each entry/format combination
- Configures plugins: Babel, TypeScript, PostCSS, Terser, etc.
- Handles externals (dependencies vs devDependencies)
- Manages source maps, compression, and name caching

5. **Build Execution**
- Sequential builds with caching (cjs format builds first)
- Watch mode available via Rollup's watch API

### Format Types

- **modern** - ES2017+ with modern syntax (async/await, arrow functions) for `<script type="module">`
- **es** (esm) - Transpiled ES modules for older bundlers
- **cjs** - CommonJS for Node.js
- **umd** - Universal Module Definition for CDNs and older environments
- **iife** - Immediately Invoked Function Expression
- **debug** - Unminified modern ES2017+ build for debugging (automatically included when `debug` field exists in package.json)

### Key Subdirectories

**src/lib/** - Utility modules:

- **babel-custom.js** - Custom Babel plugin configuration
- **package-info.js** - Reads and normalizes package.json config
- **terser.js** - Minification options normalization
- **css-modules.js** - CSS Modules configuration logic
- **compressed-size.js** - Calculates gzipped/brotli bundle sizes
- **option-normalization.js** - Parses CLI arguments (--alias, --define, etc.)

**test/fixtures/** - Integration test fixtures (each subdirectory is a test case with package.json and source)

### External Dependencies Handling

- **Externals** (src/index.js:331-357): By default, `dependencies` and `peerDependencies` are external (not bundled)
- **Bundled**: `devDependencies` are bundled into the output
- Override with `--external` flag or `--external none` to bundle everything

### Special Features

**TypeScript Support** (src/index.js:533-564):

- Automatically detected by .ts/.tsx file extension
- Uses rollup-plugin-typescript2
- Generates declaration files when `types` or `typings` is set in package.json
- Respects tsconfig.json with overrides for `module: "ESNext"` and `target: "esnext"`

**CSS Handling** (src/index.js:490-502):

- PostCSS with autoprefixer
- CSS Modules support (files ending in .module.css or via --css-modules flag)
- Output modes: external (default) or inline

**Property Mangling** (src/index.js:385-433):

- Reads from `mangle.json` or package.json `mangle`/`minify` field
- Name cache persisted across builds for consistent output

**Worker Support** (src/index.js:648):

- Bundles Web Workers via @surma/rollup-plugin-off-main-thread
- Only works with es/modern formats
- Enable with --workers flag

**Debug Format** (src/index.js:109-112, 326, 391, 607-608):

- Automatically included when `debug` field is present in package.json
- Uses modern ES2017+ compilation (same as modern format)
- Skips Terser compression/minification for readable debugging output
- Preserves variable names, whitespace, and code structure
- Output file specified by `debug` field (e.g., `"debug": "dist/my-lib.debug.js"`)

### Testing Structure

- **test/index.test.js** - Main test suite
- **test/fixtures/** - Each subdirectory contains a mini-project to build
- Tests snapshot the build output and directory structure
- Uses Jest with a custom build harness

## Common Development Patterns

### Adding New CLI Options

1. Add option definition in src/prog.js
2. Access via `options.<name>` in src/index.js
3. Pass to relevant plugin configuration in `createConfig()`

### Adding Rollup Plugins

Add to the plugins array in src/index.js:488-701, typically with conditional logic and `.filter(Boolean)` to remove falsy entries

### Debugging Builds

- Use `--no-compress` to disable minification
- Check rollup warnings in `onwarn` handler (src/index.js:469-482)
- Modern format disables Rollup cache (src/index.js:437) to prevent legacy transpilation leaking

## Important Notes

- The build is "self-hosted": microbundle builds itself (see build:self script)
- CJS format always builds first to populate cache for other formats (src/index.js:109)
- Modern format uses Babel's bugfixes mode to target browsers with `<script type="module">` support
- Shebang lines are extracted and re-added to preserve them (src/index.js:524-531, 712-713)
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<a href="#setup">Setup</a> ✯
<a href="#formats">Formats</a> ✯
<a href="#modern">Modern Mode</a> ✯
<a href="#debug">Debug Mode</a> ✯
<a href="#usage">Usage &amp; Configuration</a> ✯
<a href="#options">All Options</a>
</p>
Expand Down Expand Up @@ -144,6 +145,36 @@ The `"exports"` field can also be an object for packages with multiple entry mod
}
```

## 🐛 Debug Mode <a name="debug"></a>

Microbundle can generate an **unminified debug build** to help with debugging your library. This is useful when you need to step through your bundled code or investigate issues in production builds.

To enable debug mode, simply add a `"debug"` field to your `package.json`:

```jsonc
{
"name": "foo",
"source": "src/index.js",
"main": "./dist/foo.js",
"module": "./dist/foo.module.js",
"debug": "./dist/foo.debug.js", // 👈 Add this field
"scripts": {
"build": "microbundle"
}
}
```

When the `debug` field is present, Microbundle will automatically generate an additional unminified bundle with:

- **Modern ES2017+ syntax** (same as the `modern` format - async/await, arrow functions, etc.)
- **No minification** - readable code with preserved variable names
- **Preserved comments** - your code comments remain in the output
- **Proper formatting** - whitespace and indentation maintained

This makes it much easier to debug issues in your library code without needing to map back through source maps.

> 💡 **Tip**: The debug build is automatically included when the `debug` field exists - no need to modify the `--format` flag.

## 📦 Usage & Configuration <a name="usage"></a>

Microbundle includes two commands - `build` (the default) and `watch`.
Expand Down Expand Up @@ -172,7 +203,8 @@ The filenames and paths for generated bundles in each format are defined by the
"require": "./dist/foo.js", // CommonJS output bundle
"default": "./dist/foo.modern.mjs", // Modern ES Modules output bundle
},
"types": "dist/foo.d.ts" // TypeScript typings
"types": "dist/foo.d.ts", // TypeScript typings
"debug": "dist/foo.debug.js" // Unminified modern bundle for debugging (optional)
}
```

Expand Down
6 changes: 2 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"lodash.merge": "^4.6.2",
"magic-string": "^0.25.9",
"postcss": "^8.2.1",
"prettier": "^2.2.1",
"pretty-bytes": "^5.4.1",
"rollup": "^2.35.1",
"rollup-plugin-bundle-size": "^1.0.3",
Expand Down Expand Up @@ -131,7 +132,6 @@
"jest": "^26.6.3",
"lint-staged": "^10.5.3",
"npm-merge-driver-install": "^1.1.1",
"prettier": "^2.2.1",
"regenerator-runtime": "^0.13.7",
"rimraf": "^3.0.2",
"shell-quote": "^1.7.2",
Expand Down
31 changes: 26 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import { shouldCssModules, cssModulesConfig } from './lib/css-modules';
import { EOL } from 'os';
import MagicString from 'magic-string';
import prettier from 'prettier';

// Extensions to use when resolving modules
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.es6', '.es', '.mjs'];
Expand Down Expand Up @@ -72,7 +73,7 @@
options.pkg.name = pkgName;

if (options.sourcemap === 'inline') {
console.log(

Check warning on line 76 in src/index.js

View workflow job for this annotation

GitHub Actions / build (14.x)

Unexpected console statement

Check warning on line 76 in src/index.js

View workflow job for this annotation

GitHub Actions / build (12.x)

Unexpected console statement
'Warning: inline sourcemaps should only be used for debugging purposes.',
);
} else if (options.sourcemap === 'false') {
Expand Down Expand Up @@ -105,6 +106,12 @@
let formats = (options.format || options.formats).split(',');
// de-dupe formats and convert "esm" to "es":
formats = Array.from(new Set(formats.map(f => (f === 'esm' ? 'es' : f))));

// add debug format if pkg.debug is present and not already in the list:
if (options.pkg.debug && !formats.includes('debug')) {
formats.push('debug');
}

// always compile cjs first if it's there:
formats.sort((a, b) => (a === 'cjs' ? -1 : a > b ? 1 : 0));

Expand Down Expand Up @@ -317,6 +324,7 @@
pkg['umd:main'] || pkg.unpkg || 'x.umd.js',
mainNoExtension,
);
mainsByFormat.debug = replaceName(pkg.debug || 'x.debug.js', mainNoExtension);

return mainsByFormat[format] || mainsByFormat.cjs;
}
Expand Down Expand Up @@ -378,7 +386,8 @@
);
}

const modern = format === 'modern';
const modern = format === 'modern' || format === 'debug';
const shouldCompress = options.compress !== false && format !== 'debug';

// let rollupName = safeVariableName(basename(entry).replace(/\.js$/, ''));

Expand Down Expand Up @@ -497,7 +506,7 @@
!!writeMeta &&
options.css !== 'inline' &&
absMain.replace(EXTENSION, '.css'),
minimize: options.compress,
minimize: shouldCompress,
sourceMap: options.sourcemap && options.css !== 'inline',
}),
moduleAliases.length > 0 &&
Expand Down Expand Up @@ -586,15 +595,15 @@
custom: {
defines,
modern,
compress: options.compress !== false,
compress: shouldCompress,
targets: options.target === 'node' ? { node: '12' } : undefined,
pragma: options.jsx,
pragmaFrag: options.jsxFragment,
typescript: !!useTypescript,
jsxImportSource: options.jsxImportSource || false,
},
}),
options.compress !== false && [
shouldCompress && [
terser({
compress: Object.assign(
{
Expand Down Expand Up @@ -645,14 +654,26 @@
options.visualize && visualizer(),
// NOTE: OMT only works with amd and esm
// Source: https://github.com/surma/rollup-plugin-off-main-thread#config
useWorkerLoader && (format === 'es' || modern) && OMT(),

Check warning on line 657 in src/index.js

View workflow job for this annotation

GitHub Actions / build (14.x)

A function with a name starting with an uppercase letter should only be used as a constructor

Check warning on line 657 in src/index.js

View workflow job for this annotation

GitHub Actions / build (12.x)

A function with a name starting with an uppercase letter should only be used as a constructor
/** @type {import('rollup').Plugin} */
({
name: 'postprocessing',
// Rollup 2 injects globalThis, which is nice, but doesn't really make sense for Microbundle.
// Only ESM environments necessitate globalThis, and UMD bundles can't be properly loaded as ESM.
// So we remove the globalThis check, replacing it with `this||self` to match Rollup 1's output:
renderChunk(code, chunk, opts) {
async renderChunk(code, chunk, opts) {
// Format debug builds with Prettier for better readability
if (format === 'debug') {
const formatted = prettier.format(code, {
parser: 'babel',
...pkg.prettier,
});
return {
code: formatted,
map: null,
};
}

if (opts.format === 'umd') {
// Can swap this out with MagicString.replace() when we bump it:
// https://github.com/developit/microbundle/blob/f815a01cb63d90b9f847a4dcad2a64e6b2f8596f/src/index.js#L657-L671
Expand Down
2 changes: 1 addition & 1 deletion src/prog.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default handler => {
.option('--output, -o', 'Directory to place build files into')
.option(
'--format, -f',
`Only build specified formats (any of ${DEFAULT_FORMATS} or iife)`,
`Only build specified formats (any of ${DEFAULT_FORMATS}, debug, or iife)`,
DEFAULT_FORMATS,
)
.option('--watch, -w', 'Rebuilds on any change', false)
Expand Down
Loading