2018-05-09 08:35:25 -07:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
fix(ivy): force new imports for .d.ts files (#25080)
When ngtsc encounters a reference to a type (for example, a Component
type listed in an NgModule declarations array), it traces the import
of that type and attempts to determine the best way to refer to it.
In the event the type is defined in the same file where a reference
is being generated, the identifier of the type is used. If the type
was imported, ngtsc has a choice. It can use the identifier from the
original import, or it can write a new import to the module where the
type came from.
ngtsc has a bug currently when it elects to rely on the user's import.
When writing a .d.ts file, the user's import may have been elided as
the type was not referred to from the type side of the program. Thus,
in .d.ts files ngtsc must always assume the import may not exist, and
generate a new one.
In .js output the import is guaranteed to still exist, so it's
preferable for ngtsc to continue using the existing import if one is
available.
This commit changes how @angular/compiler writes type definitions, and
allows it to use a different expression to write a type definition than
is used to write the value. This allows ngtsc to specify that types in
type definitions should always be imported. A corresponding change to
the staticallyResolve() Reference system allows the choice of which
type of import to use when generating an Expression from a Reference.
PR Close #25080
2018-07-24 16:10:15 -07:00
|
|
|
import {Expression, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, WrappedNodeExpr, compileInjector, compileNgModule as compileR3NgModule, jitExpression} from '@angular/compiler';
|
2018-05-09 08:35:25 -07:00
|
|
|
|
2018-05-31 15:50:02 -07:00
|
|
|
import {ModuleWithProviders, NgModule, NgModuleDefInternal, NgModuleTransitiveScopes} from '../../metadata/ng_module';
|
2018-05-09 08:35:25 -07:00
|
|
|
import {Type} from '../../type';
|
2018-05-31 15:50:02 -07:00
|
|
|
import {ComponentDefInternal} from '../interfaces/definition';
|
2018-05-09 08:35:25 -07:00
|
|
|
|
|
|
|
import {angularCoreEnv} from './environment';
|
2018-06-19 11:40:29 -07:00
|
|
|
import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_INJECTOR_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from './fields';
|
|
|
|
import {reflectDependencies} from './util';
|
2018-05-09 08:35:25 -07:00
|
|
|
|
|
|
|
const EMPTY_ARRAY: Type<any>[] = [];
|
|
|
|
|
|
|
|
export function compileNgModule(type: Type<any>, ngModule: NgModule): void {
|
2018-06-06 11:23:38 -07:00
|
|
|
const declarations: Type<any>[] = flatten(ngModule.declarations || EMPTY_ARRAY);
|
2018-05-21 08:15:19 -07:00
|
|
|
|
2018-06-19 11:40:29 -07:00
|
|
|
let ngModuleDef: any = null;
|
2018-06-06 11:23:38 -07:00
|
|
|
Object.defineProperty(type, NG_MODULE_DEF, {
|
2018-05-21 08:15:19 -07:00
|
|
|
get: () => {
|
2018-06-19 11:40:29 -07:00
|
|
|
if (ngModuleDef === null) {
|
2018-05-21 08:15:19 -07:00
|
|
|
const meta: R3NgModuleMetadata = {
|
|
|
|
type: wrap(type),
|
|
|
|
bootstrap: flatten(ngModule.bootstrap || EMPTY_ARRAY).map(wrap),
|
fix(ivy): force new imports for .d.ts files (#25080)
When ngtsc encounters a reference to a type (for example, a Component
type listed in an NgModule declarations array), it traces the import
of that type and attempts to determine the best way to refer to it.
In the event the type is defined in the same file where a reference
is being generated, the identifier of the type is used. If the type
was imported, ngtsc has a choice. It can use the identifier from the
original import, or it can write a new import to the module where the
type came from.
ngtsc has a bug currently when it elects to rely on the user's import.
When writing a .d.ts file, the user's import may have been elided as
the type was not referred to from the type side of the program. Thus,
in .d.ts files ngtsc must always assume the import may not exist, and
generate a new one.
In .js output the import is guaranteed to still exist, so it's
preferable for ngtsc to continue using the existing import if one is
available.
This commit changes how @angular/compiler writes type definitions, and
allows it to use a different expression to write a type definition than
is used to write the value. This allows ngtsc to specify that types in
type definitions should always be imported. A corresponding change to
the staticallyResolve() Reference system allows the choice of which
type of import to use when generating an Expression from a Reference.
PR Close #25080
2018-07-24 16:10:15 -07:00
|
|
|
declarations: declarations.map(wrapReference),
|
|
|
|
imports: flatten(ngModule.imports || EMPTY_ARRAY)
|
|
|
|
.map(expandModuleWithProviders)
|
|
|
|
.map(wrapReference),
|
|
|
|
exports: flatten(ngModule.exports || EMPTY_ARRAY)
|
|
|
|
.map(expandModuleWithProviders)
|
|
|
|
.map(wrapReference),
|
2018-05-21 08:15:19 -07:00
|
|
|
emitInline: true,
|
|
|
|
};
|
2018-05-31 14:18:24 -07:00
|
|
|
const res = compileR3NgModule(meta);
|
2018-06-19 11:40:29 -07:00
|
|
|
ngModuleDef =
|
2018-07-16 16:36:31 -07:00
|
|
|
jitExpression(res.expression, angularCoreEnv, `ng://${type.name}/ngModuleDef.js`, []);
|
2018-05-21 08:15:19 -07:00
|
|
|
}
|
2018-06-19 11:40:29 -07:00
|
|
|
return ngModuleDef;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
let ngInjectorDef: any = null;
|
|
|
|
Object.defineProperty(type, NG_INJECTOR_DEF, {
|
|
|
|
get: () => {
|
|
|
|
if (ngInjectorDef === null) {
|
|
|
|
const meta: R3InjectorMetadata = {
|
|
|
|
name: type.name,
|
|
|
|
type: wrap(type),
|
|
|
|
deps: reflectDependencies(type),
|
|
|
|
providers: new WrappedNodeExpr(ngModule.providers || EMPTY_ARRAY),
|
|
|
|
imports: new WrappedNodeExpr([
|
|
|
|
ngModule.imports || EMPTY_ARRAY,
|
|
|
|
ngModule.exports || EMPTY_ARRAY,
|
|
|
|
]),
|
|
|
|
};
|
|
|
|
const res = compileInjector(meta);
|
2018-07-16 16:36:31 -07:00
|
|
|
ngInjectorDef = jitExpression(
|
|
|
|
res.expression, angularCoreEnv, `ng://${type.name}/ngInjectorDef.js`, res.statements);
|
2018-06-19 11:40:29 -07:00
|
|
|
}
|
|
|
|
return ngInjectorDef;
|
2018-05-21 08:15:19 -07:00
|
|
|
},
|
|
|
|
});
|
2018-06-06 11:23:38 -07:00
|
|
|
|
|
|
|
declarations.forEach(declaration => {
|
|
|
|
// Some declared components may be compiled asynchronously, and thus may not have their
|
|
|
|
// ngComponentDef set yet. If this is the case, then a reference to the module is written into
|
|
|
|
// the `ngSelectorScope` property of the declared type.
|
|
|
|
if (declaration.hasOwnProperty(NG_COMPONENT_DEF)) {
|
|
|
|
// An `ngComponentDef` field exists - go ahead and patch the component directly.
|
|
|
|
patchComponentDefWithScope(
|
2018-05-31 15:50:02 -07:00
|
|
|
(declaration as Type<any>& {ngComponentDef: ComponentDefInternal<any>}).ngComponentDef,
|
|
|
|
type);
|
2018-06-06 11:23:38 -07:00
|
|
|
} else if (
|
|
|
|
!declaration.hasOwnProperty(NG_DIRECTIVE_DEF) && !declaration.hasOwnProperty(NG_PIPE_DEF)) {
|
|
|
|
// Set `ngSelectorScope` for future reference when the component compilation finishes.
|
|
|
|
(declaration as Type<any>& {ngSelectorScope?: any}).ngSelectorScope = type;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Patch the definition of a component with directives and pipes from the compilation scope of
|
|
|
|
* a given module.
|
|
|
|
*/
|
2018-05-31 15:50:02 -07:00
|
|
|
export function patchComponentDefWithScope<C, M>(
|
|
|
|
componentDef: ComponentDefInternal<C>, module: Type<M>) {
|
2018-06-06 11:23:38 -07:00
|
|
|
componentDef.directiveDefs = () => Array.from(transitiveScopesFor(module).compilation.directives)
|
|
|
|
.map(dir => dir.ngDirectiveDef || dir.ngComponentDef)
|
|
|
|
.filter(def => !!def);
|
|
|
|
componentDef.pipeDefs = () =>
|
|
|
|
Array.from(transitiveScopesFor(module).compilation.pipes).map(pipe => pipe.ngPipeDef);
|
2018-05-09 08:35:25 -07:00
|
|
|
}
|
|
|
|
|
2018-06-06 11:23:38 -07:00
|
|
|
/**
|
|
|
|
* Compute the pair of transitive scopes (compilation scope and exported scope) for a given module.
|
|
|
|
*
|
|
|
|
* This operation is memoized and the result is cached on the module's definition. It can be called
|
|
|
|
* on modules with components that have not fully compiled yet, but the result should not be used
|
|
|
|
* until they have.
|
|
|
|
*/
|
2018-08-06 14:07:42 -07:00
|
|
|
function transitiveScopesFor<T>(moduleType: Type<T>): NgModuleTransitiveScopes {
|
2018-06-06 11:23:38 -07:00
|
|
|
if (!isNgModule(moduleType)) {
|
|
|
|
throw new Error(`${moduleType.name} does not have an ngModuleDef`);
|
|
|
|
}
|
|
|
|
const def = moduleType.ngModuleDef;
|
|
|
|
|
|
|
|
if (def.transitiveCompileScopes !== null) {
|
|
|
|
return def.transitiveCompileScopes;
|
|
|
|
}
|
|
|
|
|
|
|
|
const scopes: NgModuleTransitiveScopes = {
|
|
|
|
compilation: {
|
|
|
|
directives: new Set<any>(),
|
|
|
|
pipes: new Set<any>(),
|
|
|
|
},
|
|
|
|
exported: {
|
|
|
|
directives: new Set<any>(),
|
|
|
|
pipes: new Set<any>(),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
def.declarations.forEach(declared => {
|
|
|
|
const declaredWithDefs = declared as Type<any>& { ngPipeDef?: any; };
|
|
|
|
|
|
|
|
if (declaredWithDefs.ngPipeDef !== undefined) {
|
|
|
|
scopes.compilation.pipes.add(declared);
|
|
|
|
} else {
|
|
|
|
// Either declared has an ngComponentDef or ngDirectiveDef, or it's a component which hasn't
|
|
|
|
// had its template compiled yet. In either case, it gets added to the compilation's
|
|
|
|
// directives.
|
|
|
|
scopes.compilation.directives.add(declared);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
def.imports.forEach(<I>(imported: Type<I>) => {
|
2018-08-06 14:07:42 -07:00
|
|
|
const importedTyped = imported as Type<I>& {
|
2018-06-06 11:23:38 -07:00
|
|
|
// If imported is an @NgModule:
|
2018-05-31 15:50:02 -07:00
|
|
|
ngModuleDef?: NgModuleDefInternal<I>;
|
2018-06-06 11:23:38 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
if (!isNgModule<I>(importedTyped)) {
|
|
|
|
throw new Error(`Importing ${importedTyped.name} which does not have an ngModuleDef`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// When this module imports another, the imported module's exported directives and pipes are
|
|
|
|
// added to the compilation scope of this module.
|
|
|
|
const importedScope = transitiveScopesFor(importedTyped);
|
|
|
|
importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry));
|
|
|
|
importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry));
|
|
|
|
});
|
|
|
|
|
|
|
|
def.exports.forEach(<E>(exported: Type<E>) => {
|
|
|
|
const exportedTyped = exported as Type<E>& {
|
|
|
|
// Components, Directives, NgModules, and Pipes can all be exported.
|
|
|
|
ngComponentDef?: any;
|
|
|
|
ngDirectiveDef?: any;
|
2018-05-31 15:50:02 -07:00
|
|
|
ngModuleDef?: NgModuleDefInternal<E>;
|
2018-06-06 11:23:38 -07:00
|
|
|
ngPipeDef?: any;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Either the type is a module, a pipe, or a component/directive (which may not have an
|
|
|
|
// ngComponentDef as it might be compiled asynchronously).
|
|
|
|
if (isNgModule(exportedTyped)) {
|
|
|
|
// When this module exports another, the exported module's exported directives and pipes are
|
|
|
|
// added to both the compilation and exported scopes of this module.
|
|
|
|
const exportedScope = transitiveScopesFor(exportedTyped);
|
|
|
|
exportedScope.exported.directives.forEach(entry => {
|
|
|
|
scopes.compilation.directives.add(entry);
|
|
|
|
scopes.exported.directives.add(entry);
|
|
|
|
});
|
|
|
|
exportedScope.exported.pipes.forEach(entry => {
|
|
|
|
scopes.compilation.pipes.add(entry);
|
|
|
|
scopes.exported.pipes.add(entry);
|
|
|
|
});
|
|
|
|
} else if (exportedTyped.ngPipeDef !== undefined) {
|
|
|
|
scopes.exported.pipes.add(exportedTyped);
|
|
|
|
} else {
|
|
|
|
scopes.exported.directives.add(exportedTyped);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
def.transitiveCompileScopes = scopes;
|
|
|
|
return scopes;
|
|
|
|
}
|
|
|
|
|
|
|
|
function flatten<T>(values: any[]): T[] {
|
|
|
|
const out: T[] = [];
|
|
|
|
values.forEach(value => {
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
out.push(...flatten<T>(value));
|
|
|
|
} else {
|
|
|
|
out.push(value);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return out;
|
2018-05-09 08:35:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function expandModuleWithProviders(value: Type<any>| ModuleWithProviders): Type<any> {
|
|
|
|
if (isModuleWithProviders(value)) {
|
|
|
|
return value.ngModule;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
function wrap(value: Type<any>): Expression {
|
|
|
|
return new WrappedNodeExpr(value);
|
|
|
|
}
|
|
|
|
|
fix(ivy): force new imports for .d.ts files (#25080)
When ngtsc encounters a reference to a type (for example, a Component
type listed in an NgModule declarations array), it traces the import
of that type and attempts to determine the best way to refer to it.
In the event the type is defined in the same file where a reference
is being generated, the identifier of the type is used. If the type
was imported, ngtsc has a choice. It can use the identifier from the
original import, or it can write a new import to the module where the
type came from.
ngtsc has a bug currently when it elects to rely on the user's import.
When writing a .d.ts file, the user's import may have been elided as
the type was not referred to from the type side of the program. Thus,
in .d.ts files ngtsc must always assume the import may not exist, and
generate a new one.
In .js output the import is guaranteed to still exist, so it's
preferable for ngtsc to continue using the existing import if one is
available.
This commit changes how @angular/compiler writes type definitions, and
allows it to use a different expression to write a type definition than
is used to write the value. This allows ngtsc to specify that types in
type definitions should always be imported. A corresponding change to
the staticallyResolve() Reference system allows the choice of which
type of import to use when generating an Expression from a Reference.
PR Close #25080
2018-07-24 16:10:15 -07:00
|
|
|
function wrapReference(value: Type<any>): R3Reference {
|
|
|
|
const wrapped = wrap(value);
|
|
|
|
return {value: wrapped, type: wrapped};
|
|
|
|
}
|
|
|
|
|
2018-05-09 08:35:25 -07:00
|
|
|
function isModuleWithProviders(value: any): value is ModuleWithProviders {
|
2018-06-06 11:23:38 -07:00
|
|
|
return (value as{ngModule?: any}).ngModule !== undefined;
|
2018-05-09 08:35:25 -07:00
|
|
|
}
|
|
|
|
|
2018-05-31 15:50:02 -07:00
|
|
|
function isNgModule<T>(value: Type<T>): value is Type<T>&{ngModuleDef: NgModuleDefInternal<T>} {
|
|
|
|
return (value as{ngModuleDef?: NgModuleDefInternal<T>}).ngModuleDef !== undefined;
|
2018-05-09 08:35:25 -07:00
|
|
|
}
|