Partial Ivy + Linker — как компилируются БИБЛИОТЕКИ

Контекст (зачем вообще два режима): 00-overview. Тут — детально.

Проблема, которую решает partial compilation

Ivy-инструкции меняются от версии к версии Angular. Мы это видели: в v14 было ɵɵelementStart, в v21 — ɵɵdomElementStart; decls/vars, набор инструкций, формат defineComponent — всё это внутренний контракт рантайма конкретной версии.

Если бы библиотека публиковалась в npm в full Ivy (готовые ɵɵdefineComponent), то либа, собранная под Angular 17, могла бы не завестись на рантайме Angular 21 — инструкции бы не совпали. Это «version lock»: пришлось бы перевыпускать все либы под каждую версию Angular.

Решение: библиотека публикуется в стабильном промежуточном форматеpartial declaration. А финальные инструкции под нужную версию генерит linker уже при сборке приложения.

Что именно публикует библиотека (partial)

ng-packagr собирает либу с compilationMode: 'partial' (angularCompilerOptions в tsconfig). Тогда ngtsc для компонента вызывает НЕ compileComponentFromMetadata (full), а compileDeclareComponentFromMetadata (compiler/src/render3/partial/component.ts:66). На выходе вместо инструкций — вызов-декларация:

// в опубликованном .js библиотеки (упрощённо):
static ɵcmp = ɵɵngDeclareComponent({
  version: "21.0.3",
  type: MyButton,
  selector: "my-button",
  ngImport: i0,
  template: "<button class=\"x\">{{ label }}</button>",  // ← ШАБЛОН СТРОКОЙ, не разобран!
  isInline: true,
  // + inputs/outputs/encapsulation/styles...
});

Ключевое отличие от full: шаблон лежит строкой, выражения НЕ разобраны, никаких ɵɵdomElementStart/decls/vars. Это просто «слепок метаданных». Формат стабилен — R3DeclareComponentMetadata (compiler/src/render3/partial/api.ts) меняется редко и обратносовместимо.

Функции-декларации (по ним детектится partial-код): ɵɵngDeclareComponent, ɵɵngDeclareDirective, ɵɵngDeclarePipe, ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclareFactory, ɵɵngDeclareClassMetadata.

Что делает Linker при сборке твоего приложения

Когда ng build тянет такую либу из node_modules, в дело вступает linker — это Babel-плагин (compiler-cli/linker/babel/src/babel_plugin.ts).

Шаги:

  1. Фильтр (дёшево). needsLinking(path, source) (linker/src/file_linker/needs_linking.ts) — просто source.includes('ɵɵngDeclare…'). Файлы без деклараций пропускаются без парсинга, ради скорости.
  2. Выбор линкера. PartialLinkerSelector по имени функции и version выбирает нужный PartialLinker (для компонента — PartialComponentLinkerVersion1).
  3. Линковка. linkPartialDeclaration (partial_linkers/partial_component_linker_1.ts:80):
    const meta = this.toR3ComponentMeta(metaObj, version);   // восстановить R3ComponentMetadata
    return compileComponentFromMetadata(meta, constantPool, makeBindingParser());
    
    Внутри toR3ComponentMeta он берёт строку шаблона из декларации и снова зовёт parseTemplate(...) (partial_component_linker_1.ts:107).

То есть linker заново входит в тот же full-pipeline (01-full-pipeline, стадии 2→7): parseTemplate → ingest → фазы → reify → emit. Результат — ɵɵngDeclareComponent({...}) заменяется в коде на `ɵɵdefineComponent({...}), сгенерированный компилятором той версии Angular, которой собирается приложение.

библиотека (npm)            приложение (ng build)
ɵɵngDeclareComponent({       Babel linker plugin:
  version, type,             1. needsLinking? (includes "ɵɵngDeclare")
  template: "<...строка>",   2. выбрать PartialComponentLinkerVersion1
  inputs, ...                3. parseTemplate(строка) → compileComponentFromMetadata
})                              ▼
                            ɵɵdefineComponent({ decls, vars, template: fn,... })
                            (инструкции версии ТВОЕГО Angular)

Важная деталь про версии: linker читает поле version из декларации и включает/ выключает фичи синтаксиса под неё (partial_component_linker_1.ts:99–105): напр. блок-синтаксис @if/@for доступен только для деклараций v17+, @let — v18.1+. Так один linker корректно дотягивает либы, собранные разными версиями.

Когда linker НЕ нужен

Если ты собираешь монорепу, где либы компилируются вместе с приложением (full с самого начала) — partial/linker не участвует. Partial нужен именно для опубликованных в npm пакетов, которые пришли уже в ɵɵngDeclare-форме. Твой собственный app всегда идёт full-путём напрямую.

Итог

  1. Либы публикуются в partial (ɵɵngDeclareComponent, шаблон строкой) — формат стабилен между версиями Angular, нет version-lock.
  2. Linker (Babel-плагин) при ng build приложения находит декларации, заново зовёт parseTemplate + compileComponentFromMetadata и подменяет их на полноценные ɵɵdefineComponent под версию приложения.
  3. Поэтому код либы и твой код в итоге дают одинаковые Ivy-инструкции — см. общую картину в 00-overview.

Привязка к исходникам

  • compiler/src/render3/partial/component.tscompileDeclareComponentFromMetadata (что эмитит partial)
  • compiler/src/render3/partial/api.tsR3DeclareComponentMetadata (стабильный формат)
  • compiler-cli/linker/src/file_linker/needs_linking.ts — дешёвый фильтр
  • compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts — сама линковка
  • compiler-cli/linker/babel/src/babel_plugin.ts — встраивание в Babel/сборку