Alex Rickabaugh 235a235fab feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.

Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".

Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.

Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.

Additionally, this commit adds several unit and integration tests of various
flavors to test this change.

PR Close #22005
2018-02-12 14:34:59 -08:00

522 lines
18 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef} from '../change_detection/change_detection';
import {InjectFlags, Injector} from '../di/injector';
import {ComponentFactory, ComponentRef} from '../linker/component_factory';
import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver';
import {ElementRef} from '../linker/element_ref';
import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory';
import {TemplateRef} from '../linker/template_ref';
import {ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef, InternalViewRef, ViewRef} from '../linker/view_ref';
import {Renderer as RendererV1, Renderer2} from '../render/api';
import {Type} from '../type';
import {stringify} from '../util';
import {VERSION} from '../version';
import {callNgModuleLifecycle, initNgModule, resolveNgModuleDep} from './ng_module';
import {DepFlags, ElementData, NgModuleData, NgModuleDefinition, NodeDef, NodeFlags, Services, TemplateData, ViewContainerData, ViewData, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types';
import {markParentViewsForCheck, resolveDefinition, rootRenderNodes, splitNamespace, tokenKey, viewParentEl} from './util';
import {attachEmbeddedView, detachEmbeddedView, moveEmbeddedView, renderDetachView} from './view_attach';
const EMPTY_CONTEXT = new Object();
// Attention: this function is called as top level function.
// Putting any logic in here will destroy closure tree shaking!
export function createComponentFactory(
selector: string, componentType: Type<any>, viewDefFactory: ViewDefinitionFactory,
inputs: {[propName: string]: string} | null, outputs: {[propName: string]: string},
ngContentSelectors: string[]): ComponentFactory<any> {
return new ComponentFactory_(
selector, componentType, viewDefFactory, inputs, outputs, ngContentSelectors);
}
export function getComponentViewDefinitionFactory(componentFactory: ComponentFactory<any>):
ViewDefinitionFactory {
return (componentFactory as ComponentFactory_).viewDefFactory;
}
class ComponentFactory_ extends ComponentFactory<any> {
/**
* @internal
*/
viewDefFactory: ViewDefinitionFactory;
constructor(
public selector: string, public componentType: Type<any>,
viewDefFactory: ViewDefinitionFactory, private _inputs: {[propName: string]: string}|null,
private _outputs: {[propName: string]: string}, public ngContentSelectors: string[]) {
// Attention: this ctor is called as top level function.
// Putting any logic in here will destroy closure tree shaking!
super();
this.viewDefFactory = viewDefFactory;
}
get inputs() {
const inputsArr: {propName: string, templateName: string}[] = [];
const inputs = this._inputs !;
for (let propName in inputs) {
const templateName = inputs[propName];
inputsArr.push({propName, templateName});
}
return inputsArr;
}
get outputs() {
const outputsArr: {propName: string, templateName: string}[] = [];
for (let propName in this._outputs) {
const templateName = this._outputs[propName];
outputsArr.push({propName, templateName});
}
return outputsArr;
}
/**
* Creates a new component.
*/
create(
injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any,
ngModule?: NgModuleRef<any>): ComponentRef<any> {
if (!ngModule) {
throw new Error('ngModule should be provided');
}
const viewDef = resolveDefinition(this.viewDefFactory);
const componentNodeIndex = viewDef.nodes[0].element !.componentProvider !.nodeIndex;
const view = Services.createRootView(
injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT);
const component = asProviderData(view, componentNodeIndex).instance;
if (rootSelectorOrNode) {
view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full);
}
return new ComponentRef_(view, new ViewRef_(view), component);
}
}
class ComponentRef_ extends ComponentRef<any> {
public readonly hostView: ViewRef;
public readonly instance: any;
public readonly changeDetectorRef: ChangeDetectorRef;
private _elDef: NodeDef;
constructor(private _view: ViewData, private _viewRef: ViewRef, private _component: any) {
super();
this._elDef = this._view.def.nodes[0];
this.hostView = _viewRef;
this.changeDetectorRef = _viewRef;
this.instance = _component;
}
get location(): ElementRef {
return new ElementRef(asElementData(this._view, this._elDef.nodeIndex).renderElement);
}
get injector(): Injector { return new Injector_(this._view, this._elDef); }
get componentType(): Type<any> { return <any>this._component.constructor; }
destroy(): void { this._viewRef.destroy(); }
onDestroy(callback: Function): void { this._viewRef.onDestroy(callback); }
}
export function createViewContainerData(
view: ViewData, elDef: NodeDef, elData: ElementData): ViewContainerData {
return new ViewContainerRef_(view, elDef, elData);
}
class ViewContainerRef_ implements ViewContainerData {
/**
* @internal
*/
_embeddedViews: ViewData[] = [];
constructor(private _view: ViewData, private _elDef: NodeDef, private _data: ElementData) {}
get element(): ElementRef { return new ElementRef(this._data.renderElement); }
get injector(): Injector { return new Injector_(this._view, this._elDef); }
get parentInjector(): Injector {
let view = this._view;
let elDef = this._elDef.parent;
while (!elDef && view) {
elDef = viewParentEl(view);
view = view.parent !;
}
return view ? new Injector_(view, elDef) : new Injector_(this._view, null);
}
clear(): void {
const len = this._embeddedViews.length;
for (let i = len - 1; i >= 0; i--) {
const view = detachEmbeddedView(this._data, i) !;
Services.destroyView(view);
}
}
get(index: number): ViewRef|null {
const view = this._embeddedViews[index];
if (view) {
const ref = new ViewRef_(view);
ref.attachToViewContainerRef(this);
return ref;
}
return null;
}
get length(): number { return this._embeddedViews.length; }
createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number):
EmbeddedViewRef<C> {
const viewRef = templateRef.createEmbeddedView(context || <any>{});
this.insert(viewRef, index);
return viewRef;
}
createComponent<C>(
componentFactory: ComponentFactory<C>, index?: number, injector?: Injector,
projectableNodes?: any[][], ngModuleRef?: NgModuleRef<any>): ComponentRef<C> {
const contextInjector = injector || this.parentInjector;
if (!ngModuleRef && !(componentFactory instanceof ComponentFactoryBoundToModule)) {
ngModuleRef = contextInjector.get(NgModuleRef);
}
const componentRef =
componentFactory.create(contextInjector, projectableNodes, undefined, ngModuleRef);
this.insert(componentRef.hostView, index);
return componentRef;
}
insert(viewRef: ViewRef, index?: number): ViewRef {
if (viewRef.destroyed) {
throw new Error('Cannot insert a destroyed View in a ViewContainer!');
}
const viewRef_ = <ViewRef_>viewRef;
const viewData = viewRef_._view;
attachEmbeddedView(this._view, this._data, index, viewData);
viewRef_.attachToViewContainerRef(this);
return viewRef;
}
move(viewRef: ViewRef_, currentIndex: number): ViewRef {
if (viewRef.destroyed) {
throw new Error('Cannot move a destroyed View in a ViewContainer!');
}
const previousIndex = this._embeddedViews.indexOf(viewRef._view);
moveEmbeddedView(this._data, previousIndex, currentIndex);
return viewRef;
}
indexOf(viewRef: ViewRef): number {
return this._embeddedViews.indexOf((<ViewRef_>viewRef)._view);
}
remove(index?: number): void {
const viewData = detachEmbeddedView(this._data, index);
if (viewData) {
Services.destroyView(viewData);
}
}
detach(index?: number): ViewRef|null {
const view = detachEmbeddedView(this._data, index);
return view ? new ViewRef_(view) : null;
}
}
export function createChangeDetectorRef(view: ViewData): ChangeDetectorRef {
return new ViewRef_(view);
}
export class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
/** @internal */
_view: ViewData;
private _viewContainerRef: ViewContainerRef|null;
private _appRef: ApplicationRef|null;
constructor(_view: ViewData) {
this._view = _view;
this._viewContainerRef = null;
this._appRef = null;
}
get rootNodes(): any[] { return rootRenderNodes(this._view); }
get context() { return this._view.context; }
get destroyed(): boolean { return (this._view.state & ViewState.Destroyed) !== 0; }
markForCheck(): void { markParentViewsForCheck(this._view); }
detach(): void { this._view.state &= ~ViewState.Attached; }
detectChanges(): void {
const fs = this._view.root.rendererFactory;
if (fs.begin) {
fs.begin();
}
try {
Services.checkAndUpdateView(this._view);
} finally {
if (fs.end) {
fs.end();
}
}
}
checkNoChanges(): void { Services.checkNoChangesView(this._view); }
reattach(): void { this._view.state |= ViewState.Attached; }
onDestroy(callback: Function) {
if (!this._view.disposables) {
this._view.disposables = [];
}
this._view.disposables.push(<any>callback);
}
destroy() {
if (this._appRef) {
this._appRef.detachView(this);
} else if (this._viewContainerRef) {
this._viewContainerRef.detach(this._viewContainerRef.indexOf(this));
}
Services.destroyView(this._view);
}
detachFromAppRef() {
this._appRef = null;
renderDetachView(this._view);
Services.dirtyParentQueries(this._view);
}
attachToAppRef(appRef: ApplicationRef) {
if (this._viewContainerRef) {
throw new Error('This view is already attached to a ViewContainer!');
}
this._appRef = appRef;
}
attachToViewContainerRef(vcRef: ViewContainerRef) {
if (this._appRef) {
throw new Error('This view is already attached directly to the ApplicationRef!');
}
this._viewContainerRef = vcRef;
}
}
export function createTemplateData(view: ViewData, def: NodeDef): TemplateData {
return new TemplateRef_(view, def);
}
class TemplateRef_ extends TemplateRef<any> implements TemplateData {
/**
* @internal
*/
_projectedViews: ViewData[];
constructor(private _parentView: ViewData, private _def: NodeDef) { super(); }
createEmbeddedView(context: any): EmbeddedViewRef<any> {
return new ViewRef_(Services.createEmbeddedView(
this._parentView, this._def, this._def.element !.template !, context));
}
get elementRef(): ElementRef {
return new ElementRef(asElementData(this._parentView, this._def.nodeIndex).renderElement);
}
}
export function createInjector(view: ViewData, elDef: NodeDef): Injector {
return new Injector_(view, elDef);
}
class Injector_ implements Injector {
constructor(private view: ViewData, private elDef: NodeDef|null) {}
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
const allowPrivateServices =
this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false;
return Services.resolveDep(
this.view, this.elDef, allowPrivateServices,
{flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);
}
}
export function nodeValue(view: ViewData, index: number): any {
const def = view.def.nodes[index];
if (def.flags & NodeFlags.TypeElement) {
const elData = asElementData(view, def.nodeIndex);
return def.element !.template ? elData.template : elData.renderElement;
} else if (def.flags & NodeFlags.TypeText) {
return asTextData(view, def.nodeIndex).renderText;
} else if (def.flags & (NodeFlags.CatProvider | NodeFlags.TypePipe)) {
return asProviderData(view, def.nodeIndex).instance;
}
throw new Error(`Illegal state: read nodeValue for node index ${index}`);
}
export function createRendererV1(view: ViewData): RendererV1 {
return new RendererAdapter(view.renderer);
}
class RendererAdapter implements RendererV1 {
constructor(private delegate: Renderer2) {}
selectRootElement(selectorOrNode: string|Element): Element {
return this.delegate.selectRootElement(selectorOrNode);
}
createElement(parent: Element|DocumentFragment, namespaceAndName: string): Element {
const [ns, name] = splitNamespace(namespaceAndName);
const el = this.delegate.createElement(name, ns);
if (parent) {
this.delegate.appendChild(parent, el);
}
return el;
}
createViewRoot(hostElement: Element): Element|DocumentFragment { return hostElement; }
createTemplateAnchor(parentElement: Element|DocumentFragment): Comment {
const comment = this.delegate.createComment('');
if (parentElement) {
this.delegate.appendChild(parentElement, comment);
}
return comment;
}
createText(parentElement: Element|DocumentFragment, value: string): any {
const node = this.delegate.createText(value);
if (parentElement) {
this.delegate.appendChild(parentElement, node);
}
return node;
}
projectNodes(parentElement: Element|DocumentFragment, nodes: Node[]) {
for (let i = 0; i < nodes.length; i++) {
this.delegate.appendChild(parentElement, nodes[i]);
}
}
attachViewAfter(node: Node, viewRootNodes: Node[]) {
const parentElement = this.delegate.parentNode(node);
const nextSibling = this.delegate.nextSibling(node);
for (let i = 0; i < viewRootNodes.length; i++) {
this.delegate.insertBefore(parentElement, viewRootNodes[i], nextSibling);
}
}
detachView(viewRootNodes: (Element|Text|Comment)[]) {
for (let i = 0; i < viewRootNodes.length; i++) {
const node = viewRootNodes[i];
const parentElement = this.delegate.parentNode(node);
this.delegate.removeChild(parentElement, node);
}
}
destroyView(hostElement: Element|DocumentFragment, viewAllNodes: Node[]) {
for (let i = 0; i < viewAllNodes.length; i++) {
this.delegate.destroyNode !(viewAllNodes[i]);
}
}
listen(renderElement: any, name: string, callback: Function): Function {
return this.delegate.listen(renderElement, name, <any>callback);
}
listenGlobal(target: string, name: string, callback: Function): Function {
return this.delegate.listen(target, name, <any>callback);
}
setElementProperty(
renderElement: Element|DocumentFragment, propertyName: string, propertyValue: any): void {
this.delegate.setProperty(renderElement, propertyName, propertyValue);
}
setElementAttribute(renderElement: Element, namespaceAndName: string, attributeValue: string):
void {
const [ns, name] = splitNamespace(namespaceAndName);
if (attributeValue != null) {
this.delegate.setAttribute(renderElement, name, attributeValue, ns);
} else {
this.delegate.removeAttribute(renderElement, name, ns);
}
}
setBindingDebugInfo(renderElement: Element, propertyName: string, propertyValue: string): void {}
setElementClass(renderElement: Element, className: string, isAdd: boolean): void {
if (isAdd) {
this.delegate.addClass(renderElement, className);
} else {
this.delegate.removeClass(renderElement, className);
}
}
setElementStyle(renderElement: HTMLElement, styleName: string, styleValue: string): void {
if (styleValue != null) {
this.delegate.setStyle(renderElement, styleName, styleValue);
} else {
this.delegate.removeStyle(renderElement, styleName);
}
}
invokeElementMethod(renderElement: Element, methodName: string, args: any[]): void {
(renderElement as any)[methodName].apply(renderElement, args);
}
setText(renderNode: Text, text: string): void { this.delegate.setValue(renderNode, text); }
animate(): any { throw new Error('Renderer.animate is no longer supported!'); }
}
export function createNgModuleRef(
moduleType: Type<any>, parent: Injector, bootstrapComponents: Type<any>[],
def: NgModuleDefinition): NgModuleRef<any> {
return new NgModuleRef_(moduleType, parent, bootstrapComponents, def);
}
class NgModuleRef_ implements NgModuleData, InternalNgModuleRef<any> {
private _destroyListeners: (() => void)[] = [];
private _destroyed: boolean = false;
/** @internal */
_providers: any[];
/** @internal */
_modules: any[];
readonly injector: Injector = this;
constructor(
private _moduleType: Type<any>, public _parent: Injector,
public _bootstrapComponents: Type<any>[], public _def: NgModuleDefinition) {
initNgModule(this);
}
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
injectFlags: InjectFlags = InjectFlags.Default): any {
let flags = DepFlags.None;
if (injectFlags & InjectFlags.SkipSelf) {
flags |= DepFlags.SkipSelf;
} else if (injectFlags & InjectFlags.Self) {
flags |= DepFlags.Self;
}
return resolveNgModuleDep(
this, {token: token, tokenKey: tokenKey(token), flags: flags}, notFoundValue);
}
get instance() { return this.get(this._moduleType); }
get componentFactoryResolver() { return this.get(ComponentFactoryResolver); }
destroy(): void {
if (this._destroyed) {
throw new Error(
`The ng module ${stringify(this.instance.constructor)} has already been destroyed.`);
}
this._destroyed = true;
callNgModuleLifecycle(this, NodeFlags.OnDestroy);
this._destroyListeners.forEach((listener) => listener());
}
onDestroy(callback: () => void): void { this._destroyListeners.push(callback); }
}