Source: lib/bundler/index.js

/**
 * @file index.js
 * @description The Avenx bundler: resolve, link, shake, emit.
 *
 * ## The boundary this file sits on
 *
 * The compiler owns everything Avenx-specific — template compilation,
 * declaration processing, expression handling, Atlas, scoped CSS, the shape of
 * a generated component class. It hands the bundler *modules*: some virtual
 * (the classes it just generated), the rest real files on disk.
 *
 * The bundler owns everything module-specific — resolution, npm packages, the
 * dependency graph, dead-code elimination, format interop, the final script and
 * its source map. It knows nothing about components, bridges or pages, and it
 * must stay that way: the moment the bundler needs to know what a `.page.js` is,
 * the two halves have grown back together.
 *
 * ## Why Avenx has its own
 *
 * The same reason it has its own HTML tokenizer, its own expression parser and
 * its own reference scanner: the build is dependency-free by policy. A bundler
 * is a graph walk, a reachability analysis and a code generator, and each of
 * those is a few hundred lines when it only has to serve one compiler.
 * @module lib/bundler
 */

import { Resolver, ResolveError, isRuntimeSpecifier, AVENX_PACKAGE_ROOT } from './resolve.js';
import { buildGraph, BindingError, DynamicImportError } from './graph.js';
import { emitBundle, EmitError } from './emit.js';
import { shake } from './treeshake.js';
import { minify } from './minify.js';
import { ModuleParseError } from './parseModule.js';

export {
  Resolver,
  ResolveError,
  BindingError,
  DynamicImportError,
  EmitError,
  ModuleParseError,
  isRuntimeSpecifier,
  AVENX_PACKAGE_ROOT,
};

/**
 * Bundles an application from one or more entry modules.
 * @param {object} options - Bundle options.
 * @param {string[]} options.entries - Entry specifiers or absolute paths.
 * @param {Map<string, string>} [options.virtualModules] - Generated module id to source.
 * @param {string} options.rootDir - The project root, for relative paths.
 * @param {boolean} [options.treeShake] - Drop modules nothing needs. Default true.
 * @param {Map<string, string[]>} [options.entryNeeds] - Export names an entry's
 *   consumer requires, so a footer that reads the entry namespace keeps it.
 * @param {boolean} [options.minify] - Strip comments and indentation. Default false.
 * @param {boolean} [options.sourceMap] - Emit a source map. Default false.
 * @param {string} [options.banner] - Text before the bundle.
 * @param {string} [options.footer] - Text after the bundle.
 * @param {string} [options.file] - Output file name, used in the source map.
 * @returns {{code: string, map: object|null, stats: object}} The bundle and what it contains.
 * @throws {ResolveError|BindingError|EmitError|ModuleParseError} When the application does not link.
 */
export function bundle({
  entries,
  virtualModules = new Map(),
  rootDir,
  treeShake = true,
  entryNeeds = new Map(),
  minify: shouldMinify = false,
  sourceMap = false,
  banner = '',
  footer = '',
  file = 'bundle.js',
}) {
  const resolver = new Resolver({ virtualModules, roots: [rootDir] });
  const resolvedEntries = entries.map((entry) =>
    virtualModules.has(entry) ? entry : resolver.resolve(entry, `${rootDir}/__avenx_entry__.js`),
  );

  const { graph, order } = buildGraph({ entries: resolvedEntries, resolver });

  const included = treeShake ? shake({ graph, order, entryNeeds }) : new Set(order);

  const emitted = emitBundle({
    graph,
    order,
    included,
    banner,
    footer,
    rootDir,
    sourceMap,
    file,
  });

  // Minification preserves line count, so the map stays valid for either form.
  const code = shouldMinify ? minify(emitted.code) : emitted.code;

  return {
    code,
    map: emitted.map,
    stats: {
      modulesInGraph: graph.modules.size,
      modulesEmitted: emitted.modules,
      modulesShaken: graph.modules.size - emitted.modules,
      cycles: graph.cycles.length,
      externals: [...graph.modules.values()].filter((module) => module.external && included.has(module.id)).length,
      bytes: Buffer.byteLength(code, 'utf-8'),
    },
    graph,
    included,
  };
}