Skip to content

Latest commit

 

History

History
1468 lines (1154 loc) · 36.2 KB

File metadata and controls

1468 lines (1154 loc) · 36.2 KB

Compiler Architecture

The Stencil Compiler is the core engine that transforms TypeScript/JSX components into optimized web components. It orchestrates the entire build pipeline from source code to production-ready output.

Location: src/compiler/

Table of Contents

  1. Overview

  2. Build Journey

  3. Core Components

  4. Deep Dives

  5. Build Features

  6. Output Targets

  7. Advanced Topics

  8. Optimization & Performance

  9. Debugging & Diagnostics

  10. Development Guide

  11. Future Improvements


Quick Start

This document covers the internals of the Stencil Compiler. If you're looking for:


Overview

What is the Compiler?

The Stencil Compiler is a sophisticated build tool that:

  • Transforms modern TypeScript/JSX into standard JavaScript
  • Converts decorators into static metadata
  • Generates multiple output formats (ES modules, CommonJS, Custom Elements)
  • Optimizes code for production (tree shaking, minification, code splitting)
  • Handles styles (CSS, Sass, CSS-in-JS)
  • Manages lazy loading and hydration
  • Provides hot module replacement for development

Key Features

graph TD
    Compiler[Stencil Compiler]
    
    Compiler --> Transform[TypeScript/JSX Transform]
    Compiler --> Bundle[Module Bundling]
    Compiler --> Optimize[Production Optimization]
    Compiler --> Output[Multi-Target Output]
    Compiler --> Dev[Development Features]
    
    Transform --> Decorators[Decorator Processing]
    Transform --> JSX["JSX to h() calls"]
    Transform --> Types[Type Checking]
    
    Bundle --> Rollup[Rollup Integration]
    Bundle --> Splitting[Code Splitting]
    Bundle --> Lazy[Lazy Loading]
    
    Optimize --> TreeShake[Tree Shaking]
    Optimize --> Minify[Minification]
    Optimize --> Compress[Compression]
    
    Output --> Dist[NPM Distribution]
    Output --> WWW[Web Application]
    Output --> Custom[Custom Elements]
    
    Dev --> HMR[Hot Module Replacement]
    Dev --> Watch[File Watching]
    Dev --> Cache[Build Caching]
Loading

Architecture Overview

The compiler is built with a modular architecture:

graph TB
    subgraph "Input Layer"
        Config[Configuration]
        Source[Source Files]
        Assets[Assets]
    end
    
    subgraph "Processing Layer"
        Parser[TypeScript Parser]
        Transformer[AST Transformers]
        Bundler[Rollup Bundler]
        Optimizer[Optimizer]
    end
    
    subgraph "Output Layer"
        Generator[Output Generators]
        Writer[File Writer]
        Validator[Build Validator]
    end
    
    subgraph "Support Systems"
        Cache[Cache System]
        Workers[Worker Threads]
        Diagnostics[Diagnostics]
    end
    
    Config --> Parser
    Source --> Parser
    Parser --> Transformer
    Transformer --> Bundler
    Bundler --> Optimizer
    Optimizer --> Generator
    Generator --> Writer
    Writer --> Validator
    
    Cache -.-> Parser
    Cache -.-> Transformer
    Cache -.-> Bundler
    Workers -.-> Transformer
    Workers -.-> Bundler
    Diagnostics -.-> Parser
    Diagnostics -.-> Transformer
    Diagnostics -.-> Bundler
Loading

Build Journey

What Happens When You Run stencil build?

When you run stencil build in your terminal, you're kicking off a sophisticated compilation pipeline that transforms your modern component code into optimized, production-ready web components. Let's follow this journey step by step.

The Complete Build Flow

sequenceDiagram
    participant CLI as CLI Process
    participant Config as Config Validator
    participant Compiler as Compiler Factory
    participant TSProgram as TypeScript Program
    participant Transform as Transformers
    participant Bundle as Rollup Bundler
    participant Output as Output Targets
    participant Disk as File System

    CLI->>Config: Load & validate stencil.config.ts
    Config->>Compiler: Create compiler instance
    Compiler->>TSProgram: Initialize TypeScript builder
    
    Note over TSProgram: Discovery Phase
    TSProgram->>Transform: Find all components
    Transform->>Transform: Extract decorators
    Transform->>Transform: Parse component metadata
    
    Note over Transform: Transformation Phase
    Transform->>Transform: Convert decorators to static
    Transform->>Transform: Add lazy loading code
    Transform->>Transform: Process styles
    
    Note over Bundle: Bundling Phase
    TSProgram->>Bundle: Pass transformed modules
    Bundle->>Bundle: Create entry points
    Bundle->>Bundle: Tree shake & optimize
    
    Note over Output: Output Generation
    Bundle->>Output: Generate for each target
    Output->>Output: dist, www, custom-elements...
    Output->>Disk: Write all files
    
    Disk-->>CLI: Build complete!
Loading

Phase 1: Initialization & Configuration

When you run stencil build, the first thing that happens is the CLI entry point (src/cli/run.ts) springs into action:

// The journey begins here
export const run = async (init: CliInitOptions) => {
  const { args, logger, sys } = init;
  const flags = parseFlags(args);
  
  // Load and validate your stencil.config.ts
  const validated = await coreCompiler.loadConfig({
    config: { flags },
    configPath: foundConfig.configPath,
    logger,
    sys,
  });
  
  // Now we can start the build!
  await taskBuild(coreCompiler, validated.config);
};

The configuration loading process (src/compiler/sys/config.ts) does several important things:

  • Validates all your output targets
  • Sets up default values for missing options
  • Resolves all file paths relative to your project root
  • Prepares TypeScript compiler options

Phase 2: Creating the Compiler

Next, a compiler instance is created (src/compiler/compiler.ts):

export const createCompiler = async (userConfig: Config): Promise<Compiler> => {
  const config = getConfig(userConfig);
  const compilerCtx = new CompilerContext();
  
  // Set up in-memory file system for fast builds
  compilerCtx.fs = createInMemoryFs(sys);
  
  // Initialize build cache
  compilerCtx.cache = new Cache(config, createInMemoryFs(sys));
  
  // Patch TypeScript for Stencil's needs
  patchTypeScript(config, compilerCtx.fs);
  
  return {
    build: () => createFullBuild(config, compilerCtx),
    createWatcher: () => createWatchBuild(config, compilerCtx),
    destroy: () => compilerCtx.reset()
  };
};

The compiler maintains two important contexts:

  • CompilerContext: Persistent state across builds (cache, module map, etc.)
  • BuildContext: State for the current build (diagnostics, file changes, etc.)

Phase 3: TypeScript Compilation

Now the real magic begins. Stencil creates a TypeScript "BuilderProgram" that will handle the compilation:

graph TD
    subgraph "TypeScript Integration"
        TSConfig[tsconfig.json] --> TSProgram[TS Program]
        TSProgram --> TSHost[Watch Host]
        TSHost --> Stencil[Stencil Callback]
    end
    
    subgraph "Component Discovery"
        SourceFiles[".tsx/.ts files"] --> AST[TypeScript AST]
        AST --> Decorators["@Component"]
        Decorators --> Metadata[Component Metadata]
    end
    
    subgraph "Transformation Pipeline"
        Metadata --> T1[Decorator Transform]
        T1 --> T2[Prop Transform]
        T2 --> T3[Style Transform]  
        T3 --> T4[JSX Transform]
        T4 --> Output["JavaScript + Metadata"]
    end
Loading

The TypeScript compilation happens in src/compiler/transpile/run-program.ts:

export const runTsProgram = async (
  config: ValidatedConfig,
  compilerCtx: CompilerCtx,
  buildCtx: BuildCtx,
  tsBuilder: ts.BuilderProgram,
): Promise<boolean> => {
  // Custom transformers that convert Stencil decorators
  const transformers: ts.CustomTransformers = {
    before: [
      convertDecoratorsToStatic(config, buildCtx.diagnostics, tsTypeChecker),
      performAutomaticKeyInsertion,
    ],
    after: [
      convertStaticToMeta(config, compilerCtx, buildCtx, tsTypeChecker)
    ]
  };
  
  // Emit transformed files
  tsBuilder.emit(undefined, emitCallback, undefined, false, transformers);
  
  // Extract component metadata from transformed modules
  buildCtx.components = getComponentsFromModules(buildCtx.moduleFiles);
};

Phase 4: Component Transformation

This is where your modern component code gets transformed. Let's say you have a component like this:

@Component({
  tag: 'my-button',
  styleUrl: 'my-button.css',
  shadow: true
})
export class MyButton {
  @Prop() size: string;
  @State() clicked = false;
  
  @Event() myClick: EventEmitter;
  
  @Method()
  async doSomething() {
    // ...
  }
  
  render() {
    return <button onClick={() => this.handleClick()}>Click me!</button>;
  }
}

The transformation pipeline converts this into:

export class MyButton {
  constructor() {
    this.clicked = false;
  }
  
  async doSomething() {
    // ...
  }
  
  render() {
    return h("button", { onClick: () => this.handleClick() }, "Click me!");
  }
  
  // All decorators become static properties
  static get is() { return "my-button"; }
  static get encapsulation() { return "shadow"; }
  static get properties() {
    return {
      "size": { "type": "string", "attribute": "size" },
      "clicked": { "type": "boolean", "mutable": true }
    };
  }
  static get events() {
    return [{ "name": "myClick", "bubbles": true }];
  }
  static get methods() {
    return { "doSomething": {} };
  }
  static get style() { return MY_BUTTON_CSS; }
}

Phase 5: Module Bundling with Rollup

After TypeScript compilation, Stencil uses Rollup to bundle the modules (src/compiler/bundle/):

graph LR
    subgraph "Entry Points"
        Components[Component Modules]
        Dependencies[Dependencies]
        Styles[Styles]
    end
    
    subgraph "Rollup Processing"
        Components --> Bundle[Rollup Bundle]
        Dependencies --> Bundle
        Styles --> StylePlugin[Style Plugin]
        StylePlugin --> Bundle
    end
    
    subgraph "Optimizations"
        Bundle --> TreeShake[Tree Shaking]
        TreeShake --> Minify[Terser Minify]
        Minify --> Chunks[Code Splitting]
    end
    
    subgraph "Output Formats"
        Chunks --> ESM[ES Modules]
        Chunks --> CJS[CommonJS]
        Chunks --> SystemJS[SystemJS]
    end
Loading

The bundling configuration is built dynamically based on your output targets:

const rollupConfig = {
  input: entryPoints,
  plugins: [
    stencilCorePlugin(config, compilerCtx, buildCtx),
    nodeResolve({ browser: true }),
    commonjs(),
    json()
  ],
  treeshake: config.minifyJs,
  output: {
    format: 'es',
    sourcemap: config.sourceMap,
    chunkFileNames: config.hashFileNames ? 'p-[hash].js' : '[name]-[hash].js'
  }
};

Phase 6: Output Target Generation

This is where Stencil's flexibility shines. Based on your configuration, it generates different outputs:

graph TD
    subgraph "Build Artifacts"
        Bundles[Bundled Modules]
        Metadata[Component Metadata]
        Types[TypeScript Definitions]
    end
    
    Bundles --> DistOutput[dist Output]
    Bundles --> WwwOutput[www Output]
    Bundles --> CustomElements[Custom Elements]
    
    DistOutput --> DistFiles["/dist/<br/>- esm/<br/>- cjs/<br/>- loader/<br/>- types/"]
    
    WwwOutput --> WwwFiles["/www/<br/>- build/<br/>- index.html<br/>- host.config.json"]
    
    CustomElements --> CEFiles["/dist/components/<br/>- individual files<br/>- tree-shakeable"]
Loading

Each output target has its own generator:

WWW Output (src/compiler/output-targets/output-www.ts):

  • Generates an index.html that loads your app
  • Copies and optimizes assets
  • Embeds critical CSS inline
  • Adds preload hints

Dist Output (src/compiler/output-targets/dist-lazy/):

  • Creates npm-publishable packages
  • Generates multiple module formats (ESM, CJS)
  • Includes a loader for lazy loading
  • Creates TypeScript definitions

Custom Elements (src/compiler/output-targets/dist-custom-elements/):

  • Each component as a standalone custom element
  • No lazy loading - direct imports
  • Tree-shakeable by modern bundlers

Phase 7: File Writing & Validation

Finally, everything gets written to disk (src/compiler/build/write-build.ts):

export const writeBuild = async (
  config: ValidatedConfig,
  compilerCtx: CompilerCtx,
  buildCtx: BuildCtx,
): Promise<void> => {
  // Commit all file operations from in-memory FS to disk
  const commitResults = await compilerCtx.fs.commit();
  
  // Update build context with results
  buildCtx.filesWritten = commitResults.filesWritten;
  buildCtx.filesDeleted = commitResults.filesDeleted;
  
  // Cache successful build artifacts
  await compilerCtx.cache.commit();
  
  // Generate service workers if configured
  await outputServiceWorkers(config, buildCtx);
  
  // Validate all expected files were created
  await validateBuildFiles(config, compilerCtx, buildCtx);
};

Core Components

Compiler Entry (/src/compiler/compiler.ts)

The main compiler factory that creates compiler instances:

export const createCompiler = async (userConfig: Config): Promise<Compiler> => {
  const config = getConfig(userConfig);
  const compilerCtx = new CompilerContext();
  
  // Initialize subsystems
  compilerCtx.fs = createInMemoryFs(sys);
  compilerCtx.cache = new Cache(config, createInMemoryFs(sys));
  compilerCtx.worker = createSysWorker(config);
  
  // Patch TypeScript for Stencil
  patchTypeScript(config, compilerCtx.fs);
  
  return {
    build: () => createFullBuild(config, compilerCtx),
    createWatcher: () => createWatchBuild(config, compilerCtx),
    destroy: () => compilerCtx.reset()
  };
};

Compiler Context (/src/compiler/build/compiler-ctx.ts)

Persistent state across builds:

class CompilerContext {
  // Build state
  activeBuildId: number;
  activeFilesUpdated: string[];
  hasSuccessfulBuild: boolean;
  
  // Caching
  cache: Cache;
  moduleMap: ModuleMap;
  changedFiles: Set<string>();
  rollupCache: Map<string, RollupCache>();
  stylesheetCache: Map<string, StylesheetResult>();
  
  // File system
  fs: InMemoryFileSystem;
  
  // Component registry
  collections: CollectionCompilerMeta[] = [];
  
  // Worker management
  worker: CompilerWorkerContext;
  
  reset() {
    this.moduleMap.clear();
    this.changedFiles.clear();
    this.stylesheetCache.clear();
    // Keep rollupCache for performance
  }
}

Build Context (/src/compiler/build/build-ctx.ts)

Per-build state and results:

class BuildContext {
  // Build identity
  buildId = generateBuildId();
  timestamp = Date.now();
  
  // Build metadata
  startTime: number;
  isRebuild: boolean;
  
  // Component data
  components: ComponentCompilerMeta[] = [];
  entryModules: EntryModule[] = [];
  moduleFiles: Module[] = [];
  
  // Build stats
  filesWritten = 0;
  buildDuration = 0;
  bundleSize = 0;
  
  // Build results
  diagnostics: Diagnostic[] = [];
  buildResults: CompilerBuildResults;
  
  // File changes
  filesAdded: string[] = [];
  filesChanged: string[] = [];
  filesDeleted: string[] = [];
  
  createTimeSpan(msg: string) {
    const start = performance.now();
    return {
      finish: (finishMsg: string) => {
        const duration = performance.now() - start;
        this.debug(`${finishMsg} in ${duration}ms`);
      }
    };
  }
}

Deep Dives

Component Discovery & Metadata Extraction

When Stencil processes your source files, it needs to identify which classes are components and extract all their metadata. This happens through a sophisticated AST (Abstract Syntax Tree) analysis.

graph TD
    subgraph "Component Discovery Process"
        SourceFile[Source File] --> Parser[TS Parser]
        Parser --> AST[AST]
        AST --> Visitor[AST Visitor]
        
        Visitor --> ClassDecl{Class Declaration?}
        ClassDecl -->|Yes| HasDecorator{"Has @Component?"}
        ClassDecl -->|No| NextNode[Next Node]
        
        HasDecorator -->|Yes| ExtractMeta[Extract Metadata]
        HasDecorator -->|No| NextNode
        
        ExtractMeta --> Props["Extract @Prop"]
        ExtractMeta --> States["Extract @State"]
        ExtractMeta --> Methods["Extract @Method"]
        ExtractMeta --> Events["Extract @Event"]
        ExtractMeta --> Listeners["Extract @Listen"]
        
        Props --> BuildMeta[Build Component Metadata]
        States --> BuildMeta
        Methods --> BuildMeta
        Events --> BuildMeta
        Listeners --> BuildMeta
    end
Loading

Here's what happens when Stencil encounters a component:

Location: src/compiler/transformers/decorators-to-static/convert-decorators.ts

// Your component with decorators
@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true
})
export class MyComponent {
  @Prop() name: string;
  @State() isActive = false;
  
  @Watch('name')
  onNameChange(newValue: string, oldValue: string) {
    console.log(`Name changed from ${oldValue} to ${newValue}`);
  }
}

Gets transformed into static metadata:

export class MyComponent {
  constructor() {
    this.isActive = false;
  }
  
  static get is() { return "my-component"; }
  static get encapsulation() { return "shadow"; }
  static get originalStyleUrls() {
    return ["my-component.css"];
  }
  static get styleUrls() {
    return ["my-component.css"];
  }
  static get properties() {
    return {
      "name": {
        "type": "string",
        "member": "name",
        "reflectToAttr": false,
        "mutable": false,
        "required": false,
        "optional": false,
        "attribute": "name"
      }
    };
  }
  static get states() {
    return {
      "isActive": {}
    };
  }
  static get watchers() {
    return [{
      "propName": "name",
      "methodName": "onNameChange"
    }];
  }
}

Transform Pipeline

The transformation pipeline converts your TypeScript/JSX into a standard JavaScript output. This happens in src/compiler/transpile/run-program.ts.

graph LR
    TS[TypeScript AST] --> T1[Component Transform]
    T1 --> T2[Prop Transform]
    T2 --> T3[Style Transform]
    T3 --> T4[JSX Transform]
    T4 --> JS[JavaScript Output]
Loading

Key Transformers

Component Transformer (component-transpile.ts):

  • Converts decorators to static properties
  • Generates lazy-loading code
  • Adds runtime metadata

Native Constructor Transform (native-constructor.ts):

  • Converts ES6 classes for ES5 compatibility
  • Preserves custom element semantics

Style Transform (style-imports.ts):

  • Extracts and processes styles
  • Handles CSS modules
  • Generates scoped selectors

Lazy Loading Implementation

One of Stencil's most powerful features is lazy loading. Here's how it's implemented:

sequenceDiagram
    participant Browser
    participant Loader
    participant Registry
    participant Network
    participant Component

    Browser->>Loader: <my-component> detected
    Loader->>Registry: Check if component loaded
    Registry-->>Loader: Not loaded
    
    Loader->>Loader: Create proxy element
    Note over Loader: Proxy handles all interactions<br/>until real component loads
    
    Loader->>Network: "import('./my-component-hash.js')"
    Network-->>Loader: Component module
    
    Loader->>Component: Initialize component class
    Component->>Component: Upgrade proxy to real element
    Component->>Browser: Component ready!
Loading

The lazy loading system (src/compiler/transformers/component-lazy/lazy-component.ts) works by:

  1. Generating a Proxy Component: A lightweight proxy is created for each component
  2. Registering with the Loader: The proxy is registered with Stencil's runtime loader
  3. Dynamic Import on Demand: When the component is used, it's dynamically imported
  4. Upgrading the Element: The proxy is upgraded to the real component

Style Processing

Stencil handles styles in a sophisticated way to support both Shadow DOM and scoped CSS:

graph LR
    subgraph "Style Sources"
        CSS[.css files]
        SCSS[.scss files]
        StyleUrl[styleUrl]
        StyleString[styles string]
    end
    
    subgraph "Processing"
        CSS --> Parser[CSS Parser]
        SCSS --> SassCompiler[Sass Compiler]
        SassCompiler --> Parser
        StyleUrl --> Parser
        StyleString --> Parser
        
        Parser --> ScopeCSS{Scoped CSS?}
        ScopeCSS -->|Yes| AddScope[Add Scope IDs]
        ScopeCSS -->|No| ShadowCSS[Shadow CSS]
        
        AddScope --> Optimize[Optimize]
        ShadowCSS --> Optimize
    end
    
    subgraph "Output"
        Optimize --> ComponentStyle[Component Styles]
        Optimize --> GlobalStyle[Global Styles]
        ComponentStyle --> Bundle[Style Bundle]
        GlobalStyle --> Bundle
    end
Loading

For scoped CSS (non-Shadow DOM), Stencil adds unique scope IDs:

/* Original */
.button {
  background: blue;
}

/* Scoped */
.button.sc-my-component-h {
  background: blue;
}

Bundle Optimization

Stencil employs several strategies to create optimal bundles:

graph TD
    subgraph "Optimization Techniques"
        Components[All Components]
        
        Components --> Analysis[Dependency Analysis]
        Analysis --> Graph[Component Graph]
        
        Graph --> Grouping{Grouping Strategy}
        Grouping -->|By Feature| FeatureChunks[Feature Chunks]
        Grouping -->|By Size| SizeChunks[Size-Based Chunks]
        Grouping -->|By Route| RouteChunks[Route Chunks]
        
        FeatureChunks --> TreeShake[Tree Shaking]
        SizeChunks --> TreeShake
        RouteChunks --> TreeShake
        
        TreeShake --> Minify[Minification]
        Minify --> Output[Final Bundles]
    end
Loading

The bundling logic (src/compiler/bundle/bundle-output.ts) considers:

  1. Component Dependencies: Components that import each other are bundled together
  2. Chunk Size: Keeps chunks between 10KB-50KB for optimal loading
  3. Common Dependencies: Shared utilities are extracted to common chunks
  4. Entry Points: Each major feature can have its own entry point

Build Features

Incremental Builds

To speed up rebuilds, Stencil implements a multi-level caching system:

graph TD
    subgraph "Cache Levels"
        FileCache[File Cache]
        ModuleCache[Module Cache]
        StyleCache[Style Cache]
        BundleCache[Bundle Cache]
    end
    
    subgraph "Cache Keys"
        FileContent[File Content Hash]
        Dependencies[Dependency Graph]
        CompilerOpts[Compiler Options]
        
        FileContent --> CacheKey[Cache Key]
        Dependencies --> CacheKey
        CompilerOpts --> CacheKey
    end
    
    subgraph "Cache Usage"
        Build[Build Process] -->|Check| CacheKey
        CacheKey -->|Hit| UseCached[Use Cached Result]
        CacheKey -->|Miss| Rebuild[Rebuild Module]
        Rebuild -->|Store| UpdateCache[Update Cache]
    end
Loading

Watch Mode

const createWatchBuild = (
  config: Config,
  compilerCtx: CompilerCtx
) => {
  const watchRunner = new WatchRunner(config, compilerCtx);
  
  // Watch source files
  compilerCtx.fs.watch(config.srcDir, {
    recursive: true,
    callback: (event, filename) => {
      if (shouldRebuild(filename)) {
        watchRunner.queue(filename);
      }
    }
  });
  
  // Debounced rebuild
  let rebuildTimer: NodeJS.Timeout;
  watchRunner.on('queue', () => {
    clearTimeout(rebuildTimer);
    rebuildTimer = setTimeout(() => {
      watchRunner.start();
    }, config.watchTimeout || 200);
  });
  
  return watchRunner;
};

Caching System

To speed up rebuilds, Stencil implements a multi-level caching system:

graph TD
    subgraph "Cache Levels"
        FileCache[File Cache]
        ModuleCache[Module Cache]
        StyleCache[Style Cache]
        BundleCache[Bundle Cache]
    end
    
    subgraph "Cache Keys"
        FileContent[File Content Hash]
        Dependencies[Dependency Graph]
        CompilerOpts[Compiler Options]
        
        FileContent --> CacheKey[Cache Key]
        Dependencies --> CacheKey
        CompilerOpts --> CacheKey
    end
    
    subgraph "Cache Usage"
        Build[Build Process] -->|Check| CacheKey
        CacheKey -->|Hit| UseCached[Use Cached Result]
        CacheKey -->|Miss| Rebuild[Rebuild Module]
        Rebuild -->|Store| UpdateCache[Update Cache]
    end
Loading

Worker Architecture

class NodeWorkerController {
  workers: NodeWorkerMain[];
  taskQueue: CompilerWorkerTask[];
  
  async run(task: WorkerTask) {
    const worker = this.getAvailableWorker();
    return worker.run(task);
  }
}

Tasks distributed to workers:

  • TypeScript compilation
  • Style processing
  • Bundle generation
  • Optimization passes

Output Targets

Dist Output

For npm packages:

const outputDist = async (
  config: Config,
  compilerCtx: CompilerCtx,
  buildCtx: BuildCtx,
  outputTarget: OutputTargetDist
) => {
  // Generate entry point
  await generateDistEntry(config, buildCtx, outputTarget);
  
  // Generate loader
  await generateDistLoader(config, compilerCtx, outputTarget);
  
  // Copy collection files
  await generateDistCollection(config, compilerCtx, buildCtx, outputTarget);
  
  // Generate types
  if (outputTarget.typesDir) {
    await generateTypes(config, compilerCtx, buildCtx, outputTarget);
  }
};

WWW Output

Generates web app:

www/
├── build/           # Component builds
├── index.html       # App entry
└── host.config.json # Dev server config

Custom Elements Output

Generates standard custom elements:

// No lazy loading, direct registration
import { MyComponent } from './my-component.js';
customElements.define('my-component', MyComponent);

Creating Custom Output Targets

You can create custom output targets by implementing the OutputTarget interface and registering them in your stencil.config.ts.

interface OutputTarget {
  type: 'dist' | 'www' | 'custom' | 'docs' | 'stats';
  dir?: string;
  buildDir?: string;
}

// Generate outputs for each target
const generateOutputTargets = async (
  config: Config,
  compilerCtx: CompilerCtx,
  buildCtx: BuildCtx
) => {
  const outputTargets = config.outputTargets;
  
  await Promise.all(
    outputTargets.map(outputTarget => {
      switch (outputTarget.type) {
        case 'dist':
          return outputDist(config, compilerCtx, buildCtx, outputTarget);
        case 'www':
          return outputWww(config, compilerCtx, buildCtx, outputTarget);
        case 'custom':
          return outputTarget.generator(config, compilerCtx, buildCtx);
      }
    })
  );
};

Advanced Topics

Plugin System

Custom Stencil plugin for Rollup:

export const stencilPlugin = (): Plugin => ({
  name: 'stencil',
  
  resolveId(id) {
    // Custom resolution logic
  },
  
  load(id) {
    // Load Stencil modules
  },
  
  transform(code, id) {
    // Transform component code
  }
});

Custom Plugins

User-defined plugins:

{
  plugins: [
    sass(),
    myCustomPlugin({
      transform(code, id) {
        // Custom transformation
        return code;
      }
    })
  ]
}

Static Analysis

Dependency analysis:

interface ComponentGraph {
  // component tag -> dependent tags
  [tagName: string]: string[];
}

// Used for:
// - Optimal loading order
// - Bundle generation
// - Tree shaking

Automated documentation:

interface ComponentDoc {
  tag: string;
  description: string;
  props: PropDoc[];
  methods: MethodDoc[];
  events: EventDoc[];
  styles: StyleDoc[];
}

Prerendering

Static site generation at build time:

const prerenderConfig = {
  entryUrls: ['/'],
  hydrateOptions: {
    timeout: 10000,
    staticComponents: ['app-header', 'app-footer']
  }
};

Service Worker Generation

Automatic PWA support:

const swConfig = {
  swSrc: 'src/sw.js',
  globPatterns: ['**/*.{js,css,html}']
};

Optimization & Performance

Tree Shaking

Remove unused code:

const treeShakeBundle = (
  bundle: Bundle,
  buildCtx: BuildCtx
) => {
  const used = new Set<string>();
  
  // Mark entry points as used
  bundle.entryPoints.forEach(entry => {
    markAsUsed(entry, used, bundle.graph);
  });
  
  // Remove unused modules
  bundle.modules = bundle.modules.filter(
    module => used.has(module.id)
  );
};

Minification

Production optimizations:

const minifyBundle = async (
  code: string,
  config: Config
) => {
  if (!config.minifyJs) {
    return code;
  }
  
  const result = await terser.minify(code, {
    ecma: config.buildEs5 ? 5 : 2017,
    module: true,
    toplevel: true,
    compress: {
      passes: 2,
      global_defs: {
        'process.env.NODE_ENV': 'production'
      }
    },
    mangle: {
      properties: {
        regex: /^__/  // Mangle private properties
      }
    }
  });
  
  return result.code;
};

Code Splitting

Automatic chunking:

const createLazyChunks = (
  components: ComponentCompilerMeta[],
  buildCtx: BuildCtx
) => {
  const chunks = new Map<string, ComponentCompilerMeta[]>();
  
  components.forEach(cmp => {
    // Group by dependencies
    const chunkKey = getChunkKey(cmp, buildCtx.componentGraph);
    
    if (!chunks.has(chunkKey)) {
      chunks.set(chunkKey, []);
    }
    
    chunks.get(chunkKey).push(cmp);
  });
  
  // Create optimal chunks
  return optimizeChunks(chunks, {
    maxSize: 50000,  // 50KB max chunk
    minSize: 10000   // 10KB min chunk
  });
};

Memory Management

To prevent out of memory errors, Stencil manages memory carefully:

  • Uses in-memory file system for fast builds
  • Caches compiled modules and styles
  • Reuses Rollup cache across builds
  • Minimizes TypeScript compilation time

Debugging & Diagnostics

Debug Mode

Enable detailed debugging with:

stencil build --debug --log

This enables:

  • Component discovery details
  • Transform pipeline steps
  • Bundle generation info
  • File write operations
  • Cache hit/miss ratios
  • Build timing for each phase

Build Analysis

// Analyze build performance
const analyzeBuild = (buildCtx: BuildCtx) => {
  console.log('Build Analysis:');
  console.log(`  Duration: ${buildCtx.buildDuration}ms`);
  console.log(`  Components: ${buildCtx.components.length}`);
  console.log(`  Modules: ${buildCtx.moduleFiles.length}`);
  console.log(`  Bundles: ${buildCtx.bundles.length}`);
  console.log(`  Files Written: ${buildCtx.filesWritten}`);
  
  // Component graph
  console.log('\nComponent Dependencies:');
  buildCtx.componentGraph.forEach((deps, component) => {
    console.log(`  ${component}: ${deps.join(', ')}`);
  });
};

Common Build Errors

1. TypeScript Compilation Errors

Error: Cannot find module '@stencil/core'

// Problem
import { Component } from '@stencil/core';  // Error: Cannot find module

// Solution: Check your tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "@stencil/core": ["node_modules/@stencil/core"]
    }
  }
}

2. Component Metadata Errors

Error: Component tag must contain a hyphen

// Problem
@Component({
  tag: 'mycomponent'  // Error: needs hyphen
})

// Solution
@Component({
  tag: 'my-component'  // Valid: contains hyphen
})

3. Bundle Size Issues

If your bundles are too large:

graph TD
    Large[Large Bundle Size]
    
    Large --> Check1[Check Dependencies]
    Check1 --> Tree[Tree Shaking Working?]
    Check1 --> Imports[Check Import Paths]
    
    Large --> Check2[Analyze Bundle]
    Check2 --> Visualize[Use Bundle Visualizer]
    Check2 --> FindDups[Find Duplicates]
    
    Large --> Check3[Split Code]
    Check3 --> Lazy[Use Lazy Loading]
    Check3 --> Dynamic[Dynamic Imports]
Loading

Performance Profiling

Monitor build performance:

const stats = await compiler.build();
console.log({
  duration: stats.duration,
  componentCount: stats.componentCount,
  bundleSize: stats.bundleSize
});

Development Guide

Testing Compiler Changes

npm test src/compiler

Integration tests:

describe('compiler', () => {
  it('should compile component', async () => {
    const results = await compile({
      srcDir: './test/fixtures'
    });
    
    expect(results.hasError).toBe(false);
    expect(results.components).toHaveLength(1);
  });
});

Build Scripts

{
  "scripts": {
    "build": "node scripts/build.js",
    "build.prod": "node scripts/build.js --prod",
    "build.dev": "node scripts/build.js --dev",
    "watch": "node scripts/build.js --watch"
  }
}

Custom Build Script

// scripts/build.js
const { createNodeLogger, createNodeSys } = require('@stencil/core/compiler');

async function build() {
  const logger = createNodeLogger();
  const sys = createNodeSys();
  
  const validated = await loadConfig({
    config: {
      flags: parseArgs(process.argv.slice(2))
    },
    logger,
    sys
  });
  
  const compiler = await createCompiler(validated.config);
  
  if (validated.config.flags.watch) {
    const watcher = await compiler.createWatcher();
    process.on('SIGINT', () => watcher.close());
  } else {
    const results = await compiler.build();
    await compiler.destroy();
    
    if (results.hasError) {
      process.exit(1);
    }
  }
}

build().catch(console.error);