The library structure
Dear colleagues @BioPhoton @hoebbelsB @Karnaukhov-kh , let's discuss the library structure.
The goals we will ultimately pursue are build speed and tree-shaking.
Split big libraries
There are different reasons why we have to split big libraries into small ones. The rule of thumb is to split as much as possible. In the end we'll get a package separated into a collection of small self-contained modules, each of a manageable size and a clear-cut interface.
- [x] Library decoupling will lead to the fact that the bundler (Webpack) will not constantly keep unused dependencies in memory (e.g. we're using only the
LetDirectivebut other directives will be also bundled), and the minifier will not waste time tree-shaking unused parts every time during production build.

- [x] The
internalssubpackage can contain the API that will be never used by developers outside of the library. It can re-export all symbols with the theta symbol in the same way as Angular does, e.g.:
// cdk/internals/index.ts
export { getTNode as ɵgetTNode } from './ivy';
export { RxStrategyProvider as ɵRxStrategyProvider } from './rx-strategy-provider';
- This will decrease the coupling between different independent parts of the library since now they're bundled into a single file
- This will result in shorter compilation times for large apps
- Decoupling always results into simpler and faster testing
Tree-shaking strategies
Let's imagine that a user uses only one rendering strategy throughout the application, of course the rest of the strategies should be excluded from the production bundle, because they are not used.
Since the number of strategies can increase therefore it will increase the bundle size.
One way to make strategies tree-shakable is to go to classes and not refer to them explicitly and directly. Imagine that our strategies would look like this:
export class NoPriorityStrategy {
work() { ... }
}
export class ImmediateStrategy {
work() { ... }
}
export class UserBlockingStrategy {
work() { ... }
}
const strategies = {
noPriorityStrategy: new NoPriorityStrateg(),
immediateStrategy: new ImmediateStrategy(),
userBlockingStrategy: new UserBlockingStrategy(),
};
export class RxStrategyProvider {
schedule(work, options) {
const strategy = selectStrategy(options);
return onStrategy(...);
}
}
Whoops... We've just bundled all strategies since they're referenced explicitly. This is because the strategy is provided through a string (options.strategy), but should be provided through an InjectionToken or a class reference:
@Injectable({
providedIn: 'root',
// Default strategy.
useExisting: forwardRef(() => NoPriorityStrategy),
})
export abstract class Strategy {
abstract work(): void;
}
@Injectable({ providedIn: 'root' })
export class NoPriorityStrategy implements Strategy {
work() {}
}
@Injectable({ providedIn: 'root' })
export class ImmediateStrategy implements Strategy {
work() {}
}
@Injectable({ providedIn: 'root' })
export class UserBlockingStrategy implements Strategy {
work() {}
}
export class RxStrategyProvider {
// Will be taken from the host injector or the AppModule injector.
constructor(private strategy: Strategy) {}
schedule(work) {
return onStrategy(this.strategy);
}
}
@Directive({
providers: [RxStrategyProvider],
})
export class LetDirective {}
import { Strategy, UserBlockingStrategy } from '@rx-angular/cdk/strategies';
@Component({
template: `
<ng-template [rxLet]="observable$" let-value>{{ value }}</ng-template>
`
providers: [
{
// Override if needed.
provide: Strategy,
useExisting: UserBlockingStrategy,
},
],
})
export class SomeComponent {}
// --------------- OR ------------
export class RxStrategyProvider {
// Will be taken from the host injector or the AppModule injector.
constructor(private injector: Injector, private strategy: Strategy) {}
schedule(work, Strategy?: typeof Strategy) {
const strategy = Strategy ? this.injector.get(Strategy) : this.strategy;
return onStrategy(this.strategy);
}
}
import { UserBlockingStrategy } from '@rx-angular/cdk/strategies';
@Component({
template: `
<ng-template [rxLet]="observable$" [rxLetStrategy]="UserBlockingStrategy" let-value>{{ value }}</ng-template>
`
})
export class SomeComponent {
UserBlockingStrategy = UserBlockingStrategy;
}
- [ ] We've used the dependency inversion and we don't reference strategies directly anymore except of the default one, which should be bundled since it will be used by default.
Tree-shaking warnings and errors
Warnings and errors should be tree-shaken during production build. This can be done through the Terser global definition that's provided by Angular CLI. The below code will get into production bundle:
const tNode = getTNode(cdRef, elRef.nativeElement);
if (!tNode) {
throw new Error(
`Wasn't able to find the binding data (TNode) for the current component ${elRef.nativeElement}`
);
}
- [ ] But this will not be bundled if it's guarded with a definition:
declare const ngDevMode: boolean;
const tNode = getTNode(cdRef, elRef.nativeElement);
if (ngDevMode && !tNode) {
throw new Error(
`Wasn't able to find the binding data (TNode) for the current component ${elRef.nativeElement}`
);
}
I think most of the library structure was improved with secondary entry points. What is missing actually is the dependency inversion to allow strategies tree-shaking.