Skip to content

fix(compiler): bind setClassMetadataAsync parameters to local symbol names - #70108

Draft
mattrbeck wants to merge 1 commit into
angular:mainfrom
mattrbeck:defer-alias-symbol-binding
Draft

fix(compiler): bind setClassMetadataAsync parameters to local symbol names#70108
mattrbeck wants to merge 1 commit into
angular:mainfrom
mattrbeck:defer-alias-symbol-binding

Conversation

@mattrbeck

Copy link
Copy Markdown
Member

When @defer blocks are compiled in dev mode, setClassMetadataAsync (and partial declaration resolveMetadata) wraps a callback that replays the component's original decorator metadata. This replayed decorator refers to any deferred dependencies using the local identifiers bound in the declaring file.

Previously, R3DeferPerComponentDependency tracked a single symbolName that was used both for dereferencing the exported property on the dynamic import (m => m.Dep) and as the parameter name in the metadata callback:

import {Dep as AliasedDep} from './dep';

// Generated setClassMetadataAsync before this fix:
setClassMetadataAsync(MyApp,
  () => [import('./dep').then(m => m.Dep)],
  Dep => setClassMetadata(MyApp, [{..., imports: [AliasedDep]}], null, null));

Because the static import is stripped in favor of the dynamic loader, AliasedDep remains unbound in the callback scope, causing a runtime ReferenceError as soon as the defer block resolves. Even worse, if a component imports dependencies from two separate modules that happen to share the same export name under different local aliases (e.g. import {Dep as DepA} from './a'; import {Dep as DepB} from './b'), both parameters were generated using Dep, yielding duplicate parameter names (Dep, Dep) => ... which is a strict-mode syntax error.

In addition, removeDeferrableTypesFromComponentDecorator relied on the exported symbolName when stripping identifiers from the component decorator node, which failed to match aliased identifiers.

Solution

This PR updates R3DeferPerComponentDependency (and internal resolution tracking) to distinguish between symbolName (the module export key) and localSymbolName (the local binding identifier):

  1. Dynamic imports continue to dereference symbolName (m => m[symbolName]), leaving the dynamic loader mechanism unchanged.
  2. Metadata callbacks (setClassMetadataAsync and resolveMetadata) now bind parameters using localSymbolName.
  3. Decorator cleanup (removeDeferrableTypesFromComponentDecorator) matches against localSymbolName to correctly remove aliased imports from original decorators.

Because local identifiers are inherently unique within a file scope, parameter name collisions like (Dep, Dep) => ... are naturally prevented. For standard, non-aliased imports where localSymbolName === symbolName, the emitted code is completely unchanged, making this a safe, additive fix. Compliance test cases have been added to cover both single aliased deferred dependencies and multi-dependency export collisions.

…names

The callback wrapped by `setClassMetadataAsync` replays the component's original decorator, so it
refers to each deferred dependency by the name that the declaring file binds it to. Its parameters
were instead named after the symbols' exported names, because `R3DeferPerComponentDependency`
carried a single `symbolName` that was used both for the `m.<symbolName>` dereference inside the
dynamic import and for the parameter list. Those two uses disagree as soon as a dependency is
imported under an alias:

```ts
import {Dep as AliasedDep} from './dep';
// ...
setClassMetadataAsync(MyApp,
  () => [import('./dep').then(m => m.Dep)],
  Dep => setClassMetadata(MyApp, [{..., imports: [AliasedDep]}], null, null));
```

The static import is removed in favour of the dynamic one, so nothing binds `AliasedDep` and the
callback throws a `ReferenceError` in dev mode once the defer block loads. Two dependencies aliased
from modules that export the same name were worse still, producing a duplicate parameter list such
as `(Dep, Dep) => ...` which is a syntax error under strict mode.

`R3DeferPerComponentDependency` now carries the local name alongside the exported one. The callback
parameters bind the local name, while the dynamic import keeps dereferencing the exported name, so
the loader itself is unchanged. Local names are necessarily unique within a file, which resolves the
duplicate parameter case as well. When a dependency is not aliased the two names coincide and the
generated output is identical to before. The same applies to the `resolveMetadata` callback of the
partial declaration, and therefore to its linked output.

`removeDeferrableTypesFromComponentDecorator` is switched over for the same reason: it matches
identifiers appearing inside the decorator, which are the local ones, so aliased references were
previously not detached from the import that gets deleted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant