Skip to content

Failure to resolve exported spread-merged array of cooperating directives between Angular libraries #65686

@nsbarsukov

Description

@nsbarsukov

Which @angular/* package(s) are the source of the bug?

compiler

Is this a regression?

No

Description

This reproduction demonstrates an Angular compiler issue where a component imported from another library fails validation if its imports array contains another array built with the spread operator.

  1. Create workspace
ng new app --directory=./ --minimal --style=css --ssr=false --zoneless=true --ai-config=none
ng generate library lib-a
ng generate library lib-b
ng generate library lib-c
  1. Fill projects/lib-a/src/lib/lib-a.ts:
import { Directive } from '@angular/core';

@Directive({ selector: '[baseEntity1]' })
export class BaseEntity1 {}

@Directive({ selector: '[baseEntity2]' })
export class BaseEntity2 {}

export const BaseEntities = [
  BaseEntity1,
  BaseEntity2,
] as const;
  1. Fill projects/lib-b/src/lib/lib-b.ts:
import { Directive } from '@angular/core';
import { BaseEntities } from 'lib-a';

@Directive({selector: '[moreComplexEntity]'})
export class MoreComplexEntity {}

export const UserFriendlyImport = [...BaseEntities, MoreComplexEntity] as const;
  1. Fill projects/lib-с/src/lib/lib-c.ts:
import {Component} from '@angular/core';
import {UserFriendlyImport} from 'lib-b';

@Component({
  selector: 'comp-from-library',
  imports: [UserFriendlyImport],
  template: `
    <div baseEntity2>123</div>
    <div moreComplexEntity>123</div>
  `,
})
export class LibraryComponent {}
  1. Replace all src/app/app.ts with the following content:
import {Component} from '@angular/core';
import {LibraryComponent} from 'lib-c';

@Component({
  selector: 'app-root',
  imports: [LibraryComponent],
  template: `<comp-from-library />`,
})
export class App {}
  1. Run
ng build lib-a && ng build lib-b && ng build lib-c && ng build app

The application build fails with:

✘ [ERROR] NG2012: Component imports must be standalone components, directives, pipes, or must be NgModules. [plugin angular-compiler]

    src/app/app.ts:6:12:
      6 │   imports: [LibraryComponent],
        ╵             ~~~~~~~~~~~~~~~~
  1. Make these changes inside projects/lib-b/src/lib/lib-b.ts
- export const UserFriendlyImport = [...BaseEntities, MoreComplexEntity] as const;
+ export const UserFriendlyImport = [BaseEntity1, BaseEntity2, MoreComplexEntity] as const;

No error!
This confirms that the issue is related specifically to using the spread operator in a shared constant that is later consumed via imports.

Please provide a link to a minimal reproduction of the bug

https://github.com/nsbarsukov/ng-import-bug-repro

Please provide the exception or error you saw

✘ [ERROR] NG2012: Component imports must be standalone components, directives, pipes, or must be NgModules. [plugin angular-compiler]

    src/app/app.ts:6:12:
      6 │   imports: [LibraryComponent],
        ╵             ~~~~~~~~~~~~~~~~

Please provide the environment you discovered this bug in (run ng version)

_                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI       : 21.0.0
Angular           : 21.0.1
Node.js           : 24.10.0
Package Manager   : npm 11.6.1
Operating System  : darwin arm64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package                   │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build            │ 21.0.0            │ ^21.0.0           │
│ @angular/cli              │ 21.0.0            │ ^21.0.0           │
│ @angular/common           │ 21.0.1            │ ^21.0.0           │
│ @angular/compiler         │ 21.0.1            │ ^21.0.0           │
│ @angular/compiler-cli     │ 21.0.1            │ ^21.0.0           │
│ @angular/core             │ 21.0.1            │ ^21.0.0           │
│ @angular/forms            │ 21.0.1            │ ^21.0.0           │
│ @angular/platform-browser │ 21.0.1            │ ^21.0.0           │
│ @angular/router           │ 21.0.1            │ ^21.0.0           │
│ ng-packagr                │ 21.0.0            │ ^21.0.0           │
│ rxjs                      │ 7.8.2             │ ~7.8.0            │
│ typescript                │ 5.9.3             │ ~5.9.2            │
│ vitest                    │ 4.0.14            │ ^4.0.8            │
└───────────────────────────┴───────────────────┴───────────────────┘

Anything else?

Why we use [...] as const approach ?
In Taiga UI (Angular UI library), many apparent "single" components are actually composed from multiple components and directives. With NgModules this complexity was encapsulated: consumers imported one module and were done.

But with standalone components, this changed. Now, library consumers needed to import every individual piece manually — which wasn’t great for DX. IDEs like WebStorm started suggesting dozens of imports, and that quickly got annoying.

To restore a single, ergonomic entry point, we group related declarations into as const arrays.

For example:

export const TuiTextfield = [
  TuiLabel,
  TuiTextfieldComponent,
  TuiTextfieldDirective,
  TuiTextfieldOptionsDirective,
] as const;

This keeps the public API compact: the IDE suggests one symbol, while the array still contains all necessary pieces.

More background:
https://medium.com/angularwave/watching-angular-evolve-taiga-ui-kit-maintainers-perspective-97d2dd56b607#72b2

Why spread support is important?

The non-spread workaround:

- export const UserFriendlyImport = [...BaseEntities, MoreComplexEntity] as const;
+ export const UserFriendlyImport = [BaseEntity1, BaseEntity2, MoreComplexEntity] as const;

But in real-world projects with dozens of components and directives, manually listing each one becomes cumbersome and error-prone. The spread operator allows us to maintain modularity and reusability without sacrificing developer experience.

Just explore how we have to violate DRY in Taiga UI now only for textfield components:

That PR shows extra duplication just for one type of components. With dozens of similar components, the maintenance overhead becomes significant. Supporting spread-merged arrays in imports would allow Angular libraries to stay modular and maintainable without sacrificing DX.

Metadata

Metadata

Assignees

Labels

area: packagingIssues related to Angular's creation of npm packages

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions