GitHub - NimbleLabs/vibe-coding-bundler: A Browser-based JavaScript Bundler. Compile React, TypeScript, and JSX at runtime using esbuild-wasm.

5 min read Original article ↗

A browser-based JavaScript bundler using esbuild-wasm with import map support. Bundle JavaScript and TypeScript applications entirely in the browser without requiring a server.

Features

  • Browser-native bundling - Uses esbuild-wasm for fast, in-browser compilation
  • Import maps support - Resolve bare specifiers using the standard import maps specification
  • Virtual file system - Bundle from in-memory files without disk access
  • TypeScript & JSX - Full support for .ts, .tsx, .jsx files (transpilation only)
  • Plugin system - Extensible with onResolve and onLoad hooks
  • Multiple output formats - ESM, IIFE, and CJS output
  • Sourcemaps - Inline or external sourcemap generation
  • Tree shaking - Dead code elimination enabled by default
  • CLI included - Optional Node.js CLI for local development

Installation

npm install vibe-coding-bundler

Browser Demo

A browser demo is included in the examples/browser directory:

# Build the library first
npm run build

# Serve the examples directory
npx serve .

Open http://localhost:3000 to try the interactive bundler.

Quick Start

Browser Usage

import { createBundler, initialize } from 'vibe-coding-bundler';

// Initialize esbuild-wasm (only needed once)
await initialize();

// Create a bundler instance
const bundler = createBundler({
  // Optional: fetcher for external URLs
  fetcher: async (url) => {
    const response = await fetch(url);
    return { contents: await response.text() };
  },
});

// Bundle your code
const result = await bundler.bundle(
  '/src/index.ts',
  {
    '/src/index.ts': `
      import { useState } from 'react';
      export function App() {
        const [count, setCount] = useState(0);
        return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
      }
    `,
  },
  {
    imports: {
      react: 'https://esm.sh/react@18',
      'react/': 'https://esm.sh/react@18/',
    },
  },
  {
    format: 'esm',
    minify: true,
    sourcemap: 'inline',
  }
);

console.log(result.outputFiles); // { 'index.js': '...' }

CLI Usage

# Bundle a file
npx vibe-bundler bundle src/index.ts -o dist

# Watch mode
npx vibe-bundler watch src/index.ts -o dist

# With import map
npx vibe-bundler bundle src/index.ts --import-map import-map.json

# Minify and generate sourcemaps
npx vibe-bundler bundle src/index.ts -m --sourcemap

Import Maps

Import maps allow you to control how bare specifiers (like import 'react') are resolved.

Basic Usage

const importMap = {
  imports: {
    // Exact matches
    react: 'https://esm.sh/react@18',
    'react-dom': 'https://esm.sh/react-dom@18',

    // Prefix matches (note the trailing slash)
    'lodash/': 'https://esm.sh/lodash-es/',

    // Scoped packages
    '@scope/pkg': 'https://esm.sh/@scope/pkg',
  },
};

Scopes

Scopes allow different import mappings for different parts of your codebase:

const importMap = {
  imports: {
    lodash: 'https://esm.sh/lodash@4',
  },
  scopes: {
    '/legacy/': {
      // Files in /legacy/ use lodash v3
      lodash: 'https://esm.sh/lodash@3',
    },
  },
};

With CDN Services

Using esm.sh:

const importMap = {
  imports: {
    react: 'https://esm.sh/react@18',
    'react-dom': 'https://esm.sh/react-dom@18',
    'react-dom/client': 'https://esm.sh/react-dom@18/client',
  },
};

Using unpkg:

const importMap = {
  imports: {
    lodash: 'https://unpkg.com/lodash-es@4/lodash.js',
  },
};

Using Skypack:

const importMap = {
  imports: {
    preact: 'https://cdn.skypack.dev/preact',
  },
};

Custom Fetcher

Provide a fetcher to load external URLs:

const bundler = createBundler({
  fetcher: async (url) => {
    // Add custom headers, caching, etc.
    const response = await fetch(url, {
      headers: { 'X-Custom-Header': 'value' },
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const contents = await response.text();

    return {
      contents,
      // Optional: specify the loader
      loader: 'js',
      // Optional: base URL for relative imports in this module
      resolveDir: url.substring(0, url.lastIndexOf('/') + 1),
    };
  },
  cache: {
    maxSize: 100, // Max cached modules
    ttl: 60000, // Cache TTL in ms
  },
});

Plugins

Extend the bundler with custom resolution and loading logic:

const myPlugin = {
  name: 'my-plugin',
  setup(build) {
    // Custom resolution
    build.onResolve({ filter: /^virtual:/ }, (args) => ({
      path: args.path,
      namespace: 'virtual',
    }));

    // Custom loading
    build.onLoad({ filter: /.*/, namespace: 'virtual' }, (args) => ({
      contents: `export default "Hello from ${args.path}"`,
      loader: 'js',
    }));
  },
};

const bundler = createBundler({
  plugins: [myPlugin],
});

Built-in Plugin Helpers

import {
  createVirtualModulePlugin,
  createAliasPlugin,
  createExternalPlugin,
} from 'vibe-coding-bundler';

// Virtual modules
const virtualPlugin = createVirtualModulePlugin({
  'virtual:config': 'export default { debug: true };',
});

// Aliases
const aliasPlugin = createAliasPlugin({
  '@/utils': '/src/utils',
  lodash: 'lodash-es',
});

// Externals
const externalPlugin = createExternalPlugin(['fs', 'path', /^node:/]);

API Reference

initialize(options?)

Initialize esbuild-wasm. Safe to call multiple times.

interface InitOptions {
  wasmURL?: string; // URL to esbuild.wasm (uses CDN by default)
}

createBundler(options?)

Create a bundler instance.

interface BundlerOptions {
  plugins?: Plugin[];
  fetcher?: (url: string) => Promise<FetchResult>;
  cache?: { maxSize?: number; ttl?: number };
  baseURL?: string; // Base URL for import map resolution
}

bundler.bundle(entryPoints, files, importMap, options?)

Bundle entry points.

interface BuildOptions {
  format?: 'esm' | 'iife' | 'cjs';
  platform?: 'browser' | 'node' | 'neutral';
  minify?: boolean;
  sourcemap?: boolean | 'inline' | 'external';
  splitting?: boolean;
  target?: string | string[];
  external?: string[];
  define?: Record<string, string>;
  jsxFactory?: string;
  jsxFragment?: string;
  jsx?: 'transform' | 'preserve' | 'automatic';
  jsxImportSource?: string;
}

interface BundleResult {
  outputFiles: Record<string, string>;
  metafile?: Metafile;
  warnings: PluginMessage[];
  errors: PluginMessage[];
}

bundler.dispose()

Clean up resources and clear caches.

CLI Configuration

Create a vibe-bundler.config.js file:

export default {
  entry: ['src/index.ts'],
  outdir: 'dist',
  importMap: {
    imports: {
      react: 'https://esm.sh/react@18',
    },
  },
  buildOptions: {
    format: 'esm',
    minify: true,
    sourcemap: true,
  },
};

Or use JSON import map file:

export default {
  entry: ['src/index.ts'],
  outdir: 'dist',
  importMap: './import-map.json',
};

Security Notes

  • User Responsibility: Executing bundled output (e.g., via eval() or new Function()) is the user's responsibility. This library does not execute code by default.
  • External URLs: When using a fetcher, ensure you trust the sources being fetched.
  • Sandboxing: For untrusted code, consider running in a sandboxed iframe or Web Worker.

Browser Compatibility

  • Chrome/Edge 80+
  • Firefox 80+
  • Safari 14+

Requires support for:

  • ES2020
  • WebAssembly
  • Dynamic imports
  • Web Workers (optional, for better performance)

License

MIT

Contributing

Contributions are welcome! Please read the contributing guidelines before submitting a PR.

Acknowledgments

  • esbuild - The incredibly fast bundler that powers this library
  • WICG Import Maps - The specification this library implements