feat(compiler): type-check web components via Custom Elements Manifests - #69957
Open
JSMike wants to merge 1 commit into
Open
feat(compiler): type-check web components via Custom Elements Manifests#69957JSMike wants to merge 1 commit into
JSMike wants to merge 1 commit into
Conversation
CUSTOM_ELEMENTS_SCHEMA admits every hyphenated element and property, so applications must trade away template diagnostics to consume web components. Add the customElementsManifests compiler option to describe those elements from manifests published by their libraries. Use manifest declarations to check element and property existence, binding values, string-literal static values, event payloads, and local-reference types. Expose the same metadata through Language Service completions, quick info, documentation, and deprecation status. Treat manifest contents as untrusted input. Validate type text and exact referenced TypeScript exports before generating type-check code. Report NG4007-NG4014 diagnostics and apply narrow fail-closed fallbacks when metadata cannot be trusted or consumed CEM records are structurally inconsistent. Cache content-validated manifest loads across Language Service compiler instances without weakening resource invalidation. Summarize repeated manifest warnings by default and add the customElementsManifestsDiagnostics option for lossless itemized output. Preserve exact JavaScript property names for manifest-declared bindings while retaining native DOM remapping and sanitization. Carry the exact-name marker through full and partial compilation, linking, HMR, and backward-compatible runtime instruction arguments. Closes angular#36893
JSMike
force-pushed
the
custom-elements-manifest-support
branch
from
July 27, 2026 10:48
b21990d to
541af79
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Web components are currently second-class citizens in Angular templates. Angular's built-in schema
mechanism,
CUSTOM_ELEMENTS_SCHEMA, does not describe custom elements — it disables schemachecking for every hyphenated tag name, so misspelled tags, unknown properties, invalid binding
values, and mistyped
$eventpayloads all compile silently.Angular does not currently consume the ecosystem-standard
custom-elements.jsonmetadata thatweb component libraries publish. As a result, the compiler and Angular Language Service cannot
provide component-like template checking, completions, documentation, or event and local-reference
types for those elements.
Issue Number: #36893
Closes #36893
What is the new behavior?
This PR adds an
angularCompilerOptions.customElementsManifestsoption that ingestsCustom Elements Manifest
(
custom-elements.json) files — the ecosystem-standard machine-readable description that mostmajor web component libraries already publish — and integrates the declared elements into
Angular's template type-checking system. Custom elements described by a manifest are checked like
supported native elements: unknown tags and unknown bindings are diagnosed, binding values and
$eventare type-checked under strict templates, and the Angular Language Service offerscompletions, hovers, documentation, and deprecation status for them. Applications using such
libraries no longer need
CUSTOM_ELEMENTS_SCHEMAin AOT-compiled application templates.Schema v1 and v2 manifests are supported. Angular is explicitly a projection reader: it strictly
validates the CEM records used for template checking without claiming whole-document JSON Schema
validation. An invalid semantic version or unknown major produces one summarizable NG4014 warning,
then Angular reads the known projection best-effort.
If a vendor manifest is defective, the option can point to a corrected manifest controlled by the
consumer instead. A relative replacement can use self-contained types or explicit
package/modulereferences; a local workspace package can also provide package identity forstrictly typed local references. A corrected subset can intentionally precede the vendor manifest:
duplicate tags are first-wins, and the expected NG4010 names both configured entries. Relative
entries always resolve against the final project's tsconfig directory, including when the option
is inherited through
extends.Manifest warnings are summarized per manifest by default — a large design system with dozens
of affected declarations produces one warning with a count and examples rather than dozens of
lines. The companion option
customElementsManifestsDiagnostics: 'summary' | 'verbose'restoresindividual reporting; Angular CLI versions with the companion builder change also map
--verboseto it for one run. Errors are never summarized.Does this PR introduce a breaking change?
The feature is opt-in. Projects that do not configure
customElementsManifestsretain theirexisting compiler, runtime, schema, and Language Service behavior.
CUSTOM_ELEMENTS_SCHEMAandNO_ERRORS_SCHEMAremain available and retain their existing behavior. Configuring a manifestintentionally enables stricter checking for the tags it declares.
Other information
Prior discussion
This feature was proposed in the talk "Custom Elements Everywhere: The Lost Levels"
at the Angular Enterprise Summit (March 19, 2026) and discussed there with Alex Rickabaugh @alxhub:
proposal slide, with reproductions in
JSMike/angular-custom-elements-lost-levels.
It is also a long-standing community ask:
#12045 (custom schemas RFC, open since 2016),
#36893 (Language Service integration with
custom-elements.json, resolved by this PR), and#58483 (typed element definitions);
#36355 (exact property names) is fixed for
manifest-declared properties. A fuller issue-by-issue mapping is in
angular-cem-related-open-issues.md.What this enables
With a configured manifest, for its declared elements:
the manifest doesn't declare produce NG8002 with a manifest-aware message.
readonlymembersare excluded (assigning to them can never work). Attribute-only declarations do not authorize
same-named property bindings. A
custom-element-definitionexport whose declaration cannot beresolved still registers its tag with a closed, empty member schema plus an NG4013 warning: the
tag is recognized, but unknown bindings remain NG8002 errors. Applications that deliberately
need permissive handling can opt into
CUSTOM_ELEMENTS_SCHEMAseparately.declared types when those types are trustworthy (see Security model) — primitives, literal
unions, inline object types, and exact public types resolved through the manifest's
type.referencesto the package's actual TypeScript declarations. Named globals requirepackage: 'global:'. Static values are checked only for explicit string-literal unions; CEMdoes not define universal number or boolean attribute converters. Number and boolean property
bindings remain fully typed.
$eventis typed from the manifest's eventtype;manifest events take precedence over colliding native events on that element, while directive
outputs still win and
(window:…)/(document:…)targets remain native.#el) retain the web component's class typeinstead of
HTMLElementwhen the class is importable from the package's public API.completions for static attributes, quick-info with the manifest's documentation, type text,
default values, and deprecation strikethrough. Manifest load results are cached across the
compiler instances the language service creates per edit (content-validated, including unsaved
manifest and declaration editor buffers, referenced globals, and module-resolution inputs),
keeping steady post-edit completion latency around a tenth of a second for multi-megabyte
manifest sets instead of re-parsing them on every keystroke.
the registry's HTML attribute→property remapping so the exact JavaScript property named by the
manifest is set. Partial declarations that use this emit
minVersion: '22.1.0'; the linker andruntime instruction signatures are extended backward-compatibly (a new optional trailing
argument), so existing compiled output is unaffected.
Manifest metadata is compile-time-only. Components compiled at runtime — classic
Karma/webpack TestBed templates, templates introduced by
TestBed.overrideComponent, and JITbootstrap — still need
CUSTOM_ELEMENTS_SCHEMAwhen Angular's runtime checks cannot recognize thecustom element. Angular's modern Vitest builder AOT-compiles tests and uses the manifest normally.
When both a manifest and
CUSTOM_ELEMENTS_SCHEMAare present, the manifest remains authoritativefor its own tags while the schema continues to cover other hyphenated tags — supporting
incremental migration.
Reading guide
The diff is one commit but decomposes into five layers, reviewable in this order. (They are
separable into a PR series if preferred — each layer is independently green — at the cost that
only the combination is meaningfully testable against real component libraries.)
packages/compiler/src/schema/custom_elements_manifest_schema.ts,dom_element_schema_registry.ts): plain serializable schema types and an optionalDomElementSchemaRegistryconstructor argument that seeds custom-element entries from[HTMLElement]. No behavior change when unused;@angular/compilerstays free of file I/O.packages/compiler-cli/src/ngtsc/custom_elements_manifest/): resolver(three entry forms, resolved strictly through the project's module resolution), projection parser, the
check-type validator (
check_type.ts— the security boundary), and the loader that verifiesevery emitted type reference against the package's real TypeScript declarations, plus
NgCompilerwiring, incremental-rebuild dependencies, and diagnostics NG4007–NG4014.packages/compiler/src/typecheck/ops/,packages/compiler-cli/src/ngtsc/typecheck/): contextual assignments for property bindingsand static attributes,
$eventparameter typing, instance-typeddocument.createElementcasts, and manifest-aware NG8001/NG8002 messages.
packages/compiler/src/template/pipeline/,packages/core/src/render3/instructions/, partial linker): the optionalexactDomPropertyNameargument onproperty/twoWayProperty, reified only formanifest-declared tags.
packages/language-service/): completions, quick info, and thelarge-manifest resource handling.
Security model
Manifest type text is untrusted input, and validated check types are spliced verbatim into
generated type-check code. The validation boundary is
custom_elements_manifest/src/check_type.ts, which only passes through text where everycharacter and identifier is vetted: a character whitelist (no statements, comments, escapes,
decorators, or template interpolation), an identifier whitelist limited to TypeScript type
keywords, exact-coverage substitution of named types via the manifest's
type.referencesintoimport("<pkg>").Namequeries, and a final parse proving the result isexactly one syntactically valid type. Anything else fails closed to existence-only checking and
produces a bounded NG4013 warning. The loader then additionally verifies that every emitted
import()specifier resolves to realTypeScript declarations that export the referenced name as a type — so generated code never
contains an unvalidated or unresolvable reference. Relaxing any of these rules is called out in
the code as requiring security review. Named platform and default-library types are not guessed:
they require an explicit
type.referencesentry withpackage: "global:".Same-package module paths first resolve package-relative. If that fails, Angular tries the path
relative to the manifest directory because the CEM schema identifies a module path but does not
unambiguously define its base for a manifest published below the package root. Both paths still go
through normal module resolution and exact export validation.
Diagnostics
Narrow, fail-closed recovery so one bad declaration never erases unrelated valid schemas:
customElements.define.Within a manifest, a declaration may own only one tag and registration is first-wins. Its single
matching home-module definition export may refine a bare
tagNameseed without warning; latersame-tag definitions produce NG4010, while a conflicting second tag produces NG4014. Duplicate
diagnostics identify both the retained and ignored registration origins. Across manifests, the
configured array order remains authoritative and NG4010 identifies both entries.
Validation against real libraries
A public
reproduction and review harness
contains the complete setup and verification workflow: building the
custom-elements-manifest-supportfork, installing its snapshot packages, running compiler/build/test checks, and manually reviewing
the locally built VS Code extension. It also documents the optional, separately scoped
cem-cli-followupsworkflow.The harness integrates seventeen manifests covering twelve published design systems —
Shoelace, Vaadin, UI5, Fluent UI, Calcite, Adobe Spectrum, Nord, Clarity, Red Hat Design System (the
first CEM v2.1 producer), Auro, PatternFly, and Material Web — plus the published
@box-model/webpackage, focused hand-written Lion, Material Web, and Spectrum button manifests, and a
consumer-authored local workspace package. Results:
deliberately narrow safe-emission boundary (unshipped
src/*.tsmodule paths, missing exports,cross-package references, unreferenced named types, and unsupported function/qualified types).
Each degrades exactly as designed: affected types fall back to existence-only checking, nothing
crashes, and no false errors reach templates. Default (summary) output: 25 CEM warnings; verbose
diagnostics: 1,802 individually itemized (19 NG4009 + 330 NG4011 + 1,153 NG4013 +
300 NG4014). Repeated definition records for the same declaration/tag identity are deduplicated;
genuinely conflicting registrations retain deterministic first-wins behavior. Notably,
Auro's manifest — whose module paths all name unpublished
src/*.jssources but whose declaredtypes are self-contained primitives — degrades to two bounded warnings: instance types fall back
to
HTMLElement, and the missing definition export is reported, while every declared propertyand property-binding value check remains active.
a missing name by guessing an alias or
defaultexport. Element-instance identity is separate:its order-independent export selection can use a sole unambiguous alias/default, and when one
declaration is re-exported exact-name from both its own module and a sibling barrel (the Red Hat
Design System pattern), the declaration's own home module is the authoritative tiebreak. This
recovers real local-reference types without reinterpreting
type.references.exportsmaps are respected as authored: a manifest reference whose module the exportmap does not expose stays existence-only even when a declaration file physically ships inside
the package (measured at 141 references across Clarity, Calcite, and Fluent in this gallery).
This is a deliberate standards-first decision — recovering types from files a package does not
publicly export would build on packaging the author never committed to. The root causes are
library packaging defects with small fixes, tracked separately as candidate upstream
contributions.
type.referenceishonored only in the specification's whole-text form, so references nested in compound type
text without exact
start/endindices — a common pattern in UI5's generator output (139 inthis gallery) — fail closed with NG4013 rather than being recovered heuristically. Producer
conformance fixes are tracked upstream. A whole-text reference remains structurally valid, but
still requires the exact named public export; UI5's
ButtonDesigncurrently points at a modulethat exports only
default, so its property check also fails closed while the separate staticstring-literal attribute union remains strict.
default | loading | success | error) is rejected with a bounded NG4013 warning and degrades toexistence-only checking. A corrected consumer-controlled manifest can replace the vendor entry;
Angular does not silently substitute
.d.tstypes for defective CEM metadata.bindings, unknown properties, unknown tags, kebab/camel attribute-property mapping,
$eventtyping where references are complete, literal-union completions, and instance-typed local
references. Static number/boolean spellings are deliberately converter-agnostic; equivalent
property bindings retain strict
number/booleanchecking.package entry is discovered through
package.json#customElements, its literal-union property ischecked, and its template local reference resolves to the package's exported element class.
checking despite publishing no CEM, while a focused Spectrum button projection replaces vendor
named unions that otherwise degrade to existence-only checking. The replacement recovers
variant/treatmentvalue checks and removes ten vendor metadata warnings from the gallery.Testing
check-type validator (security matrix:
injection attempts, span mismatches, qualified names, index-less references), loader
(resolution forms, stripping, diagnostics), registry.
as stringmaterialization, symbol-shim emission for the Language Service, exact-name emission).
opt-outs, precedence rules), NgCompiler manifest reload/incremental tests, HMR, linker.
manifest larger than the TypeScript server file limit, and cross-compiler manifest-cache
behavior (reuse identity; manifest creation, content, and deletion; entry order; compiler
options; ambient globals; declaration/export and failed-lookup changes; custom-resolver bypass;
diagnostics-mode isolation).
registration, malformed consumed shapes, schema-version projection, exact LS property versus
attribute quick info,
on*rejection, and manifest-only full/partial emit updates.Out of scope / known follow-ups
Inheritance and mixin expansion are intentionally out of scope, matching the adev docs: inherited
members are recognized only when the manifest lists them on the custom element's own declaration
(as manifests produced by the Custom Elements Manifest analyzer do);
superclassandmixinsreferences are not resolved. Spectrum's inherited
disabled— absent fromsp-button's owndeclaration — is the live example in the demo gallery, which binds the declared
pendingmemberinstead. CEM
slots,cssParts, andcssPropertiesdescribe styling and composition surfacesthat have no Angular template-type-checking counterpart, so they are ignored.
CUSTOM_ELEMENTS_SCHEMAbehavior is unchanged. Known follow-up work is tracked separately fromthis PR. The remaining items are separable diagnostics, i18n, and metadata follow-ups rather than
correctness blockers for manifest ingestion. Export-map-hidden
declarations, package ownership for path-loaded manifests, and cross-manifest export mapping are
deliberately not follow-up work: all are resolved upstream by the affected libraries
publishing what their manifests reference, per the standards-first decisions exercised by the
public review harness.
Design detail worth reviewer input — manifest-owned
on*properties. Angular currently rejectsevery case-insensitive
on*property binding before consulting the element schema, preserving theDOM event-property XSS guard. This means a standards-valid field such as Auro's
onDarkcannot bebound as
[onDark], although itsondarkattribute and[attr.ondark]remain available. Thepre-PR contract documents and pins that behavior instead of weakening the security check. A future
change could consult exact manifest/schema ownership before the broad heuristic while continuing
to reject exact lower-case native handler properties, but that design requires framework security
review.
Known limitation — i18n-marked exact property names. The normal template pipeline preserves
exact manifest property names. The i18n attribute opcode currently calls the shared property setter
without the exact-name flag, so a translated interpolated property such as
i18n-forcan beremapped to
htmlForat runtime. This is an existing narrow i18n opcode gap; it is documented forthe initial feature and should be fixed by threading the exact-name decision through those opcodes.
Design detail worth reviewer input — navigable manifest diagnostics. Manifest diagnostics
currently travel the file-less compiler-options channel, so editors cannot jump to the defective
manifest entry. Measurement rejected retaining offset JSON ASTs (~11× manifest size in steady
heap; 61 MB for one 5.5 MB manifest), so the planned follow-up computes spans by a lazy one-shot
re-parse and delivers navigation as LSP
DiagnosticRelatedInformation— a clickablefile:line link into
custom-elements.jsonattached to the existing diagnostic, rather thanpublishing squiggles into a JSON document the Angular language server does not own (which would
take on cross-server diagnostic lifecycle for that file, and has no delivery vehicle at all in
the pull model). We consider the related-information link good enough for now; if reviewers
prefer in-file squiggles, that ownership/lifecycle question should be settled before the
diagnostic API shape is committed.
Two-way binding on a custom element retains Angular's existing DOM semantics. A manually written
[(value)]="value"listens forvalueChangeand assigns$eventdirectly tovalue. This is thesame behavior custom elements have under
CUSTOM_ELEMENTS_SCHEMA: most web components do not emitan Angular-style
valueChangeevent, and a DOMCustomEventsupplies the event object rather thanits
detailpayload. CEM support does not introduce or conceal this limitation—the LanguageService deliberately does not offer two-way completions for manifest properties. Use separate
property and event bindings to extract the component's documented event payload. A possible
general custom-element diagnostic belongs in follow-up work rather than this manifest-ingestion
PR.
Companion Angular CLI PR: the Angular CLI half of the diagnostics UX ships separately — the
application builder maps
--verbosetocustomElementsManifestsDiagnostics: 'verbose', and watchrebuilds suppress unchanged manifest warnings. This PR is fully functional without it (the
compiler option is authoritative); the adev docs qualify the CLI shortcut accordingly. The
implementation is available for testing on the
cem-cli-followupsbranch asseparate
feat(@angular/build)and
fix(@angular/build)commits. Its pull request should wait until this framework feature is accepted. The pre-existing
same-build
--verbosedouble-print remains a separate CLI issue and is not changed by eithercommit.