skip to content

An Angular app bundled without the Angular CLI crashes with 'JIT compilation failed' in a component from an npm library; how do partial compilation and the linker explain it?

level: seniorimportance: should knowfreq 28%

answer

  1. libraries are compiled only halfway
  2. ɵɵngDeclareComponent in the published code
  3. someone must finish compilation
  4. a Babel plugin outside the CLI

basics

~20 s

Published Angular libraries use compilationMode partial, emitting stable ɵɵngDeclare* calls instead of private Ivy instructions. The app's build must run the Angular linker to finish them; a bundler that skips it leaves a JIT fallback that fails without @angular/compiler.

solid answer

~50 s

Ivy instructions are private and can change between patch releases, so a library meant for npm is built with `"compilationMode": "partial"` (the library schematic puts it in `tsconfig.lib.prod.json`). Partial output keeps each class's metadata, including the template as a string, in calls such as `ɵɵngDeclareComponent({ minVersion, version, type, selector, template, ... })`. The application's build then runs the **Angular linker**, which turns those declarations into full `ɵɵdefineComponent` definitions with the app's own compiler version. The Angular CLI does this automatically. A custom webpack or other bundler must add the linker itself, usually as the Babel plugin from `@angular/compiler-cli/linker/babel`. If it is skipped, `ɵɵngDeclareComponent` runs at runtime and asks for the JIT compiler; without `@angular/compiler` loaded the app throws, with a message saying the library was partially compiled and the linker has not processed it. The fix is to add the linker, not to ship the compiler.

code

ts · 16 lines
ts
// webpack.config.mjs of an app that does not use the Angular CLI
import linkerPlugin from '@angular/compiler-cli/linker/babel';

export default {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        use: {
          loader: 'babel-loader',
          options: { plugins: [linkerPlugin], compact: false, cacheDirectory: true },
        },
      },
    ],
  },
};

go deeper

for a junior

Know that published Angular libraries are only partly compiled and that the app's build finishes compiling them.

for a middle

Explain compilationMode partial versus full, what a ɵɵngDeclareComponent call contains, and that the CLI runs the linker for you.

for a senior

Read the JIT-fallback error as a missing linker, add the Babel linker plugin to a custom build, and state the app-version-at-least-library-version rule.

for a principal

Decide how a shared component library is built and versioned across many apps: partial output for npm consumers, full output only for a same-version monorepo.

## Why libraries cannot ship full Ivy output A full AoT build emits calls such as `ɵɵdefineComponent`, `ɵɵelementStart` and `ɵɵproperty`. These `ɵɵ` instructions are **private**: Angular's documentation warns that they are not public API and may change between **patch** versions. An app compiled with Angular 22.2.1 that loads a library compiled with 22.2.0 would be mixing two instruction sets. Angular therefore offers two compilation modes, set in `angularCompilerOptions`: | `compilationMode` | Output | Use it for | | :-- | :-- | :-- | | `'full'` (default) | Final Ivy definitions and instructions | Applications, and libraries built from source with the exact same Angular version | | `'partial'` | Stable intermediate declarations | Libraries published to npm | The CLI's library schematic generates a `tsconfig.lib.prod.json` that sets `"compilationMode": "partial"`, so `ng build my-lib` produces partial-Ivy output by default in production. ## What partial output looks like Instead of instructions, each class receives **declaration calls**: - `ɵɵngDeclareFactory` for `ɵfac`, - `ɵɵngDeclareComponent`, `ɵɵngDeclareDirective`, `ɵɵngDeclarePipe`, `ɵɵngDeclareInjectable`, `ɵɵngDeclareNgModule`, `ɵɵngDeclareInjector`, - `ɵɵngDeclareClassMetadata` for the dev-mode decorator metadata. Each call receives an object with a `minVersion`, the `version` that produced it, the class `type`, its selector, inputs and outputs, and for components the **template as source text**. The template has been parsed and validated, but it has not been turned into instructions yet. ## The linker finishes the job The **Angular linker** runs during the *application's* build. It finds every `ɵɵngDeclare*` call in `node_modules` code and replaces it with the full definition, compiled by the application's own Angular compiler. The result: every library and the app use one consistent set of Ivy instructions. - **With the Angular CLI**, the linker is integrated into the build pipeline; nothing needs configuring. - **Without the CLI**, the linker is available as a Babel plugin imported from `@angular/compiler-cli/linker/babel`, typically registered through `babel-loader` for `.mjs`/`.js` files. It supports caching, so each library is linked once. ## Diagnosing the crash When a custom bundler skips the linker, the declarations reach the browser unchanged. `ɵɵngDeclareComponent` then asks for the JIT compiler. Without `@angular/compiler` loaded, the runtime throws an error that spells out the cause: 1. *The component 'X' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available.* 2. *The component is part of a library that has been partially compiled.* 3. *However, the Angular Linker has not processed the library such that JIT compilation is used as fallback.* Importing `@angular/compiler` makes the error disappear, but only by compiling every library component in the browser on every load - the same cost AoT exists to remove. The correct fix is to add the linker to the build. ## Version rules - The app's Angular version must be **the same as or newer than** the version used to build any library it depends on. Partial-Ivy libraries work in apps from Angular 12 onwards. - Each declaration records a `minVersion`. If the app's linker cannot satisfy it, the linker reports that the application depends on a library published with a newer Angular and suggests upgrading; by default this is an error. - Full-Ivy libraries require the **exact** same Angular version as the app, which is only realistic in a monorepo built from source. - The old View Engine library format stopped working in Angular 16, when the compatibility compiler `ngcc` was removed. ## One more partial-mode trap: import cycles In an application, an NgModule-declared component whose template would require a cyclic import can be wired up through *remote scoping*: extra side-effectful code on the NgModule. That code defeats tree-shaking, so partial compilation refuses it and reports `NG3003`. Library authors fix it by breaking the cycle: moving shared types into their own file, using `import type`, or co-locating the classes. ## Checking the fix - Search the built bundle for `ɵɵngDeclare`: after linking, no such calls should remain; they are replaced by `ɵɵdefineComponent` and friends. - Confirm `@angular/compiler` is **not** in the production bundle; its presence usually means something still needs JIT. - Keep the linker cache directory between builds so each library version is linked only once. - When upgrading Angular, upgrade the application first or together with its libraries, never a library ahead of the app.

  • Why is importing @angular/compiler the wrong fix for an unlinked Angular library?
    It works by compiling every partially compiled component in the browser on each load: the compiler ships in the bundle, startup slows, and template problems appear at runtime. That reintroduces exactly the costs AoT removes. Adding the linker to the build produces the same full definitions the CLI would, once, at build time.
  • When is it acceptable to build an Angular library with compilationMode full?
    When the library is never published and is always built from source together with the app on the exact same Angular version, as in a monorepo. Full output contains private Ivy instructions that are only guaranteed to match the compiler version that produced them.
  • What does NG3003 mean when building an Angular library in partial mode?
    A component's template uses a class whose import would create an import cycle. Applications can resolve that with remote scoping, but that code is side-effectful and prevents tree-shaking, so partial compilation rejects it. Break the cycle with a shared file, `import type`, or by putting the classes in one file.

saying these in an interview costs you the question

  • Partial compilation means the library ships uncompiled TypeScript sources.
  • A library and the app must always use the exact same Angular patch version.
  • Adding import '@angular/compiler' is the proper fix for an unlinked library.
  • The Angular CLI needs a plugin configured before it can consume partial-Ivy libraries.
  • Libraries should publish full Ivy output because it is faster to install.