Parcel Web Build Tool

repository·v2·Indexed 13 days ago

https://github.com/parcel-bundler/parcel

A zero-configuration web build tool designed for speed and scalability. Parcel supports HTML, CSS, JavaScript, and various assets out of the box, featuring a Rust-based compiler, automatic production optimizations like tree-shaking and minification, and a powerful plugin system. The ecosystem includes specialized tools such as parcel-resolver for Node.js module resolution, parcel-query for investigating build graphs, and the @parcel/optimizer-inline-requires experimental plugin.

Tokens
107.9K
Snippets
384
Records
495
Agent score
99%

What's inside Parcel

  1. Overview of Parcel features

    v2

    Parcel provides several core capabilities for modern web development:

    • Zero Config: Supports many languages and file types (HTML, CSS, JS, images, fonts, etc.) without requiring a configuration file to start.
    • Performance: Uses a Rust-based JavaScript compiler and parallelizes builds using worker threads. It utilizes caching to avoid rebuilding unchanged code.
    • Production Optimizations: Automatically handles tree-shaking, minification (JS, CSS, HTML), image optimization, content hashing, and code splitting.
    • Targeting: Automatically transforms code for specific environments, including support for modern/legacy browsers, JSX, and TypeScript.
    • Extensibility: While it works with zero config, it features a powerful plugin system and a simple configuration format for complex build requirements.
  2. Use @parcel/transformer-babel for Babel transformations

    v2

    The @parcel/transformer-babel plugin allows Parcel to transform assets using Babel. It automatically detects and uses your existing Babel configuration files (like .babelrc or babel.config.js) using the same resolution logic as Babel itself.

    If no configuration file is found, the transformer uses a default configuration that includes:

    • @babel/preset-env: Uses targets from package.json or defaults. It applies to source code and installed packages with higher browser targets than your app.
    • @babel/plugin-flow-strip-types: Strips Flow types if a Flow directive is detected.
    • @babel/plugin-transform-typescript: Enabled for .ts and .tsx files.
    • @babel/plugin-transform-react-jsx: Enabled for .jsx files or if a React-like dependency is found in package.json.
  3. How Parcel skips unused assets during bundling

    v2

    Parcel can omit unused assets from the final bundle using two methods:

    Subgraph Skipping

    If a re-export is unused, the entire dependency subgraph can be ignored. This happens via:

    • Deferring: During the graph visit, if a re-export (e.g., export {x} from './y') has no incoming dependencies requesting x, the asset is skipped. This prevents unnecessary transformations of the dependency.
    • Unused Dependency: If bundleGraph.getUsedSymbols(dep).size === 0, the dependency is skipped. This is enabled by symbol propagation during scope hoisting and supports export * syntax.

    Single Asset Skipping

    A side-effect-free asset that only contains re-exports (and is not imported by other bundles) can be skipped entirely by the JS packager, as its re-exports are resolved directly to the original assets.

    Example of skipping:

    import {a} from './lib.js'; // 'a' is used
    console.log(a);
    
    // lib.js
    export * from './exports-a.js'; // 'exports-a.js' is used
    export * from './exports-b.js'; // 'exports-b.js' is skipped (symbol propagation)
    export {c} from './exports-c.js'; // 'exports-c.js' is skipped (deferring)
    import {a} from './lib.js';
    console.log(a);
    
    // lib.js, asset gets skipped
    export * from './exports-a.js'; // dep used, not skipped
    export * from './exports-b.js'; // dep skipped with symbol propagation
    export {c} from './exports-c.js'; // dep skipped with deferring
  4. Understand SWC scope hoisting transformations

    v2

    Parcel uses SWC scope hoisting to optimize module loading by flattening the module tree. This process transforms various module patterns (ESM, CJS, and mixed) into a format that can be efficiently packaged.

    Depending on the input module type and how it is accessed, the transformation results in different outputs:

    • Static patterns: If require or import statements are statically analyzable, they are replaced with direct variable references to the exported symbols.
    • Non-static patterns: If modules are accessed via dynamic keys (e.g., require('x')[something]), the transformer uses a namespace object to preserve the module's interface.
    • Wrapped patterns: If require is called inside a function or as part of a conditional expression, the module is wrapped to ensure side effects occur in the correct order.
  5. How AdjacencyList uses NodeTypeMap and EdgeTypeMap

    v2

    The AdjacencyList does not use SharedTypeMap directly. Instead, it manages the graph by interacting with two specialized subclasses:

    1. NodeTypeMap (internally nodes): Manages node-specific data.
    2. EdgeTypeMap (internally edges): Manages edge-specific data.

    Core Operations:

    • Adding edges: Linking records in the nodes map with records in the edges map.
    • Deleting edges: Unlinking records between the two maps.
    • Resizing: Automatically expanding either map when capacity is reached.
    • Traversing: Following links from node records to edge records (and vice versa) to navigate the graph.
  6. How symbol resolution works in the Scopehoisting Packager

    v2

    Symbol resolution determines the exact code used to access an exported value. The getSymbolResolution() method (and the underlying bundleGraph.getSymbolResolution()) returns the resolved expression for a symbol.

    Possible resolution results include:

    • $id$export$bar: A same-bundle ESM import.
    • $id$exports: A same-bundle ESM import (namespace).
    • id$exports.bar: A non-statically analyzable export.
    • parcelRequire("id").bar: A wrapped asset or an asset in a different bundle.
    • $parcel$interopDefault: Used when an ESM default import resolves to a non-statically analyzable CJS asset.

    Key Behaviors:

    • Interop Handling: It automatically handles CJS/ESM interop (e.g., using the namespace if a default import resolves to a CJS asset).
    • Hoisted Requires: It tracks imports of wrapped assets by mutating a hoistedRequires list, ensuring necessary parcelRequire calls are prepended.
    • Transitive Re-exports: The underlying bundleGraph.getSymbolResolution() recursively traverses re-exports to find the actual value, allowing imports to point to the original source rather than just a re-exporting binding.
  7. How undeferring assets works

    v2

    If a previously unused dependency is discovered during the transformation process (for example, when a new import is added to a file), Parcel performs undeferring to ensure the new asset is included in the bundle.

    When a new dependency is added to the graph:

    1. Parcel's AssetGraphRequest traversal includes an override to revisit nodes that have the hasDeferred=true flag.
    2. This triggers a re-evaluation of the parent assets (e.g., AssetLib).
    3. If shouldVisitChild and shouldDeferDependency determine the dependency is now used, Parcel calls unmarkParentsWithHasDeferred.
    4. This clears the hasDeferred flags up the tree and allows the traversal to visit the previously skipped asset group, triggering its transformation and inclusion in the graph.
  8. How Tree Shaking and Scope Hoisting work in Parcel

    v2

    Parcel uses two primary mechanisms to reduce bundle size and improve performance:

    1. Tree Shaking: Removes unused code by identifying unused exports. Parcel achieves this through symbol propagation and conditional generation of $parcel$export() calls. To ensure minifiers can safely remove unused functions, use the /*#__PURE__*/ comment on side-effect-free calls (e.g., React.createContext()).

    2. Scope Hoisting: Instead of using a module registry (the "prelude") where imports are function calls like parcelRequire("id").foo, Parcel concatenates assets into a single scope. This replaces module lookups with direct variable access (e.g., $id$export$foo), which improves minification (inlining/constant evaluation) and reduces bundle overhead.

    Example of Scope Hoisting transformation:

    // math.js
    export function add(a, b) { return a + b; }
    export function square(a) { return a * a; }
    
    // index.js
    import {add} from './math';
    console.log(add(2, 3));
    
    // Becomes (simplified):
    function $fa6943ce8a6b29$export$add(a, b) { return a + b; }
    function $fa6943ce8a6b29$export$square(a) { return a * a; } // Dead code
    console.log($fa6943ce8a6b29$export$add(2, 3));
    // math.js
    export function add(a, b) {
      return a + b;
    }
    
    export function square(a) {
      return a * a;
    }
    
    // index.js
    import {add} from './math';
    console.log(add(2, 3));
  9. How Parcel handles ESM and CJS Interop

    v2

    Parcel manages the integration of ES Modules (ESM) and CommonJS (CJS) through several mechanisms:

    Default Import Interop

    When importing a CommonJS module via a synchronous ESM import, the default import typically contains the module.exports object.

    To support modules transpiled from ESM to CJS (which use exports.__esModule = true), Parcel uses an interop check. If an asset is statically identified as ESM or ESM-transpiled-to-CJS, Parcel can optimize away the interopRequireDefault helper call.

    // Standard interop pattern used by transpilers
    function interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : {default: obj};
    }

    Handling Conditional Requires and Multiple Bundles

    Parcel uses the parcelRequire registry in two specific scenarios where pure ESM concatenation is impossible:

    1. Conditional Requires: If an asset has a conditional incoming dependency (e.g., require() inside an if block), the asset is wrapped in parcelRequire.register. Inside these subgraphs, imports are replaced with parcelRequire calls rather than top-level variables to ensure side effects run correctly.
    2. Runtime Deduplication: When an asset is shared across multiple bundles (e.g., via dynamic import()), the registry ensures the asset is evaluated only once, preserving identity (e.g., instanceof checks) and preventing duplicate side effects.
    // Example of CJS interop
    import v from './other';
    // v == { x: 2, y: 3 }
    
    // other.js
    module.exports.x = 2;
    module.exports.y = 3;
  10. Handle Dynamic Imports in Scopehoisting

    v2

    Dynamic imports (e.g., import('./other.js')) are handled specially to allow for tree-shaking while still providing a namespace object at runtime.

    Instead of listing every possible symbol in the dependency's symbol map (which would prevent removing unused symbols), the transformer uses a promiseSymbol stored in dep.meta.promiseSymbol. This identifier is used by the packager to replace the import() call.

    Example:

    Input:

    import('./other.js').then(({foo}) => log(foo));

    Generated Dependency Metadata:

    {
      "promiseSymbol": "$assetId$importAsync$other",
      "symbols": {
        "foo": {
          "local": "$assetId$importAsync$other$90a7f3efeed30595"
        }
      }
    }

    Generated Code:

    import 'assetId:21eb38ddd81971f9';
    $assetId$importAsync$other.then(({foo}) => log(foo));
    import('./other.js').then(({foo}) => log(foo));
  11. Work with Identifiers and Scopes in swc

    v2

    Identifiers (JsWord)

    Identifiers in swc use the JsWord type rather than String.

    • To create a JsWord from an arbitrary string, use .into(): let x: JsWord = "something".into();.
    • For hard-coded, interned words (like require, URL, default, eval), use the js_word! macro for better efficiency: let y = js_word!("require");.

    Scopes and Hygiene

    swc uses SyntaxContext (a unique number) to handle variable hygiene. A unique variable binding is defined by the pair (JsWord, SyntaxContext).

    • To store a unique binding, use the Id type, which represents this pair.
    • Use the helper method ident.to_id() to convert an identifier to its unique Id.

    Caution with visit_ident

    The visit_ident (or fold_ident) functions target all Ident nodes. This includes not just variable bindings, but also names used in destructuring, member access, and private class variables. Modifying all Ident nodes globally can have unintended side effects across different contexts.

      let x: JsWord = "something".into();
      let y: JsWord = js_word!("require") // or "URL", "default", "eval", ...
    
      let ident: Ident; // the ast node
      ident.sym // the JsWord "string"
      ident.span.ctxt // the syntax context
    
      // Using Id for unique bindings
      let id = ident.to_id();
  12. How parcel-resolver works

    v2

    The parcel-resolver crate implements the Node.js module resolution algorithm, supporting both CommonJS and ES modules. It includes advanced features common in the JavaScript ecosystem, such as:

    • TypeScript tsconfig paths and extension rewriting.
    • alias and browser fields used by bundlers.
    • Absolute and tilde (~) paths.

    These features can be toggled individually using feature flags. The resolver works by using a Cache to store file system information and returns a resolution along with Invalidations that indicate which files should trigger cache invalidation.