feat(tsc-wrapped): add an option to `ngc` to bundle metadata (#14509)
Closes #14509
This commit is contained in:
parent
9a6f3d637f
commit
3b896709a9
|
@ -10,6 +10,8 @@ export {MetadataWriterHost} from './src/compiler_host';
|
|||
export {CodegenExtension, UserError, main} from './src/main';
|
||||
|
||||
export {default as AngularCompilerOptions} from './src/options';
|
||||
export * from './src/bundler';
|
||||
export * from './src/cli_options';
|
||||
export * from './src/collector';
|
||||
export * from './src/index_writer';
|
||||
export * from './src/schema';
|
||||
|
|
|
@ -0,0 +1,558 @@
|
|||
/**
|
||||
* @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 * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {MetadataCollector} from './collector';
|
||||
|
||||
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataArray, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMethodMetadata} from './schema';
|
||||
|
||||
// The character set used to produce private names.
|
||||
const PRIVATE_NAME_CHARS = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
||||
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
|
||||
];
|
||||
|
||||
interface Symbol {
|
||||
module: string;
|
||||
name: string;
|
||||
|
||||
// Produced by indirectly by exportAll() for symbols re-export another symbol.
|
||||
exports?: Symbol;
|
||||
|
||||
// Produced by indirectly by exportAll() for symbols are re-exported by another symbol.
|
||||
reexportedAs?: Symbol;
|
||||
|
||||
// Produced by canonicalizeSymbols() for all symbols. A symbol is private if it is not
|
||||
// exported by the index.
|
||||
isPrivate?: boolean;
|
||||
|
||||
// Produced by canonicalizeSymbols() for all symbols. This is the one symbol that
|
||||
// respresents all other symbols and is the only symbol that, among all the re-exported
|
||||
// aliases, whose fields can be trusted to contain the correct information.
|
||||
// For private symbols this is the declaration symbol. For public symbols this is the
|
||||
// symbol that is exported.
|
||||
canonicalSymbol?: Symbol;
|
||||
|
||||
// Produced by canonicalizeSymbols() for all symbols. This the symbol that originally
|
||||
// declared the value and should be used to fetch the value.
|
||||
declaration?: Symbol;
|
||||
|
||||
// A symbol is referenced if it is exported from index or referenced by the value of
|
||||
// a referenced symbol's value.
|
||||
referenced?: boolean;
|
||||
|
||||
// Only valid for referenced canonical symbols. Produces by convertSymbols().
|
||||
value?: MetadataEntry;
|
||||
|
||||
// Only valid for referenced private symbols. It is the name to use to import the symbol from
|
||||
// the bundle index. Produce by assignPrivateNames();
|
||||
privateName?: string;
|
||||
}
|
||||
|
||||
export interface BundleEntries { [name: string]: MetadataEntry; }
|
||||
|
||||
export interface BundlePrivateEntry {
|
||||
privateName: string;
|
||||
name: string;
|
||||
module: string;
|
||||
}
|
||||
|
||||
export interface BundledModule {
|
||||
metadata: ModuleMetadata;
|
||||
privates: BundlePrivateEntry[];
|
||||
}
|
||||
|
||||
export interface MetadataBundlerHost { getMetadataFor(moduleName: string): ModuleMetadata; }
|
||||
|
||||
type StaticsMetadata = {
|
||||
[name: string]: MetadataValue | FunctionMetadata;
|
||||
};
|
||||
|
||||
export class MetadataBundler {
|
||||
private symbolMap = new Map<string, Symbol>();
|
||||
private metadataCache = new Map<string, ModuleMetadata>();
|
||||
private exports = new Map<string, Symbol[]>();
|
||||
private rootModule: string;
|
||||
|
||||
constructor(
|
||||
private root: string, private importAs: string|undefined, private host: MetadataBundlerHost) {
|
||||
this.rootModule = `./${path.basename(root)}`;
|
||||
}
|
||||
|
||||
getMetadataBundle(): BundledModule {
|
||||
// Export the root module. This also collects the transitive closure of all values referenced by
|
||||
// the exports.
|
||||
const exportedSymbols = this.exportAll(this.rootModule);
|
||||
this.canonicalizeSymbols(exportedSymbols);
|
||||
// TODO: exports? e.g. a module re-exports a symbol from another bundle
|
||||
const entries = this.getEntries(exportedSymbols);
|
||||
const privates = Array.from(this.symbolMap.values())
|
||||
.filter(s => s.referenced && s.isPrivate)
|
||||
.map(s => ({
|
||||
privateName: s.privateName,
|
||||
name: s.declaration.name,
|
||||
module: s.declaration.module
|
||||
}));
|
||||
return {
|
||||
metadata:
|
||||
{__symbolic: 'module', version: VERSION, metadata: entries, importAs: this.importAs},
|
||||
privates
|
||||
};
|
||||
}
|
||||
|
||||
static resolveModule(importName: string, from: string): string {
|
||||
return resolveModule(importName, from);
|
||||
}
|
||||
|
||||
private getMetadata(moduleName: string): ModuleMetadata {
|
||||
let result = this.metadataCache.get(moduleName);
|
||||
if (!result) {
|
||||
if (moduleName.startsWith('.')) {
|
||||
const fullModuleName = resolveModule(moduleName, this.root);
|
||||
result = this.host.getMetadataFor(fullModuleName);
|
||||
}
|
||||
this.metadataCache.set(moduleName, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private exportAll(moduleName: string): Symbol[] {
|
||||
const module = this.getMetadata(moduleName);
|
||||
let result: Symbol[] = this.exports.get(moduleName);
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = [];
|
||||
|
||||
const exportSymbol = (exportedSymbol: Symbol, exportAs: string) => {
|
||||
const symbol = this.symbolOf(moduleName, exportAs);
|
||||
result.push(symbol);
|
||||
exportedSymbol.reexportedAs = symbol;
|
||||
symbol.exports = exportedSymbol;
|
||||
};
|
||||
|
||||
// Export all the symbols defined in this module.
|
||||
if (module && module.metadata) {
|
||||
for (let key in module.metadata) {
|
||||
const data = module.metadata[key];
|
||||
if (isMetadataImportedSymbolReferenceExpression(data)) {
|
||||
// This is a re-export of an imported symbol. Record this as a re-export.
|
||||
const exportFrom = resolveModule(data.module, moduleName);
|
||||
this.exportAll(exportFrom);
|
||||
const symbol = this.symbolOf(exportFrom, data.name);
|
||||
exportSymbol(symbol, key);
|
||||
} else {
|
||||
// Record that this symbol is exported by this module.
|
||||
result.push(this.symbolOf(moduleName, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export all the re-exports from this module
|
||||
if (module && module.exports) {
|
||||
for (const exportDeclaration of module.exports) {
|
||||
const exportFrom = resolveModule(exportDeclaration.from, moduleName);
|
||||
// Record all the exports from the module even if we don't use it directly.
|
||||
this.exportAll(exportFrom);
|
||||
if (exportDeclaration.export) {
|
||||
// Re-export all the named exports from a module.
|
||||
for (const exportItem of exportDeclaration.export) {
|
||||
const name = typeof exportItem == 'string' ? exportItem : exportItem.name;
|
||||
const exportAs = typeof exportItem == 'string' ? exportItem : exportItem.as;
|
||||
exportSymbol(this.symbolOf(exportFrom, name), exportAs);
|
||||
}
|
||||
} else {
|
||||
// Re-export all the symbols from the module
|
||||
const exportedSymbols = this.exportAll(exportFrom);
|
||||
for (const exportedSymbol of exportedSymbols) {
|
||||
const name = exportedSymbol.name;
|
||||
exportSymbol(exportedSymbol, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.exports.set(moduleName, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill in the canonicalSymbol which is the symbol that should be imported by factories.
|
||||
* The canonical symbol is the one exported by the index file for the bundle or definition
|
||||
* symbol for private symbols that are not exported by bundle index.
|
||||
*/
|
||||
private canonicalizeSymbols(exportedSymbols: Symbol[]) {
|
||||
const symbols = Array.from(this.symbolMap.values());
|
||||
const exported = new Set(exportedSymbols);
|
||||
symbols.forEach(symbol => {
|
||||
const rootExport = getRootExport(symbol);
|
||||
const declaration = getSymbolDeclaration(symbol);
|
||||
const isPrivate = !exported.has(rootExport);
|
||||
const canonicalSymbol = isPrivate ? declaration : rootExport;
|
||||
symbol.isPrivate = isPrivate;
|
||||
symbol.declaration = declaration;
|
||||
symbol.canonicalSymbol = canonicalSymbol;
|
||||
});
|
||||
}
|
||||
|
||||
private getEntries(exportedSymbols: Symbol[]): BundleEntries {
|
||||
const result: BundleEntries = {};
|
||||
|
||||
const exportedNames = new Set(exportedSymbols.map(s => s.name));
|
||||
let privateName = 0;
|
||||
|
||||
function newPrivateName(): string {
|
||||
while (true) {
|
||||
let digits: string[] = [];
|
||||
let index = privateName++;
|
||||
let base = PRIVATE_NAME_CHARS;
|
||||
while (!digits.length || index > 0) {
|
||||
digits.unshift(base[index % base.length]);
|
||||
index = Math.floor(index / base.length);
|
||||
}
|
||||
digits.unshift('\u0275');
|
||||
const result = digits.join('');
|
||||
if (!exportedNames.has(result)) return result;
|
||||
}
|
||||
}
|
||||
|
||||
exportedSymbols.forEach(symbol => this.convertSymbol(symbol));
|
||||
|
||||
Array.from(this.symbolMap.values()).forEach(symbol => {
|
||||
if (symbol.referenced) {
|
||||
let name = symbol.name;
|
||||
if (symbol.isPrivate && !symbol.privateName) {
|
||||
name = newPrivateName();
|
||||
;
|
||||
symbol.privateName = name;
|
||||
}
|
||||
result[name] = symbol.value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private convertSymbol(symbol: Symbol) {
|
||||
const canonicalSymbol = symbol.canonicalSymbol;
|
||||
|
||||
if (!canonicalSymbol.referenced) {
|
||||
canonicalSymbol.referenced = true;
|
||||
const declaration = canonicalSymbol.declaration;
|
||||
const module = this.getMetadata(declaration.module);
|
||||
if (module) {
|
||||
const value = module.metadata[declaration.name];
|
||||
if (value && !declaration.name.startsWith('___')) {
|
||||
canonicalSymbol.value = this.convertEntry(declaration.module, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private convertEntry(moduleName: string, value: MetadataEntry): MetadataEntry {
|
||||
if (isClassMetadata(value)) {
|
||||
return this.convertClass(moduleName, value);
|
||||
}
|
||||
if (isFunctionMetadata(value)) {
|
||||
return this.convertFunction(moduleName, value);
|
||||
}
|
||||
return this.convertValue(moduleName, value);
|
||||
}
|
||||
|
||||
private convertClass(moduleName: string, value: ClassMetadata): ClassMetadata {
|
||||
return {
|
||||
__symbolic: 'class',
|
||||
arity: value.arity,
|
||||
extends: this.convertExpression(moduleName, value.extends),
|
||||
decorators:
|
||||
value.decorators && value.decorators.map(d => this.convertExpression(moduleName, d)),
|
||||
members: this.convertMembers(moduleName, value.members),
|
||||
statics: value.statics && this.convertStatics(moduleName, value.statics)
|
||||
};
|
||||
}
|
||||
|
||||
private convertMembers(moduleName: string, members: MetadataMap): MetadataMap {
|
||||
const result: MetadataMap = {};
|
||||
for (const name in members) {
|
||||
const value = members[name];
|
||||
result[name] = value.map(v => this.convertMember(moduleName, v));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private convertMember(moduleName: string, member: MemberMetadata) {
|
||||
const result: MemberMetadata = {__symbolic: member.__symbolic};
|
||||
result.decorators =
|
||||
member.decorators && member.decorators.map(d => this.convertExpression(moduleName, d));
|
||||
if (isMethodMetadata(member)) {
|
||||
(result as MethodMetadata).parameterDecorators = member.parameterDecorators &&
|
||||
member.parameterDecorators.map(
|
||||
d => d && d.map(p => this.convertExpression(moduleName, p)));
|
||||
if (isConstructorMetadata(member)) {
|
||||
if (member.parameters) {
|
||||
(result as ConstructorMetadata).parameters =
|
||||
member.parameters.map(p => this.convertExpression(moduleName, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private convertStatics(moduleName: string, statics: StaticsMetadata): StaticsMetadata {
|
||||
let result: StaticsMetadata = {};
|
||||
for (const key in statics) {
|
||||
const value = statics[key];
|
||||
result[key] = isFunctionMetadata(value) ? this.convertFunction(moduleName, value) : value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private convertFunction(moduleName: string, value: FunctionMetadata): FunctionMetadata {
|
||||
return {
|
||||
__symbolic: 'function',
|
||||
parameters: value.parameters,
|
||||
defaults: value.defaults && value.defaults.map(v => this.convertValue(moduleName, v)),
|
||||
value: this.convertValue(moduleName, value.value)
|
||||
};
|
||||
}
|
||||
|
||||
private convertValue(moduleName: string, value: MetadataValue): MetadataValue {
|
||||
if (isPrimitive(value)) {
|
||||
return value;
|
||||
}
|
||||
if (isMetadataError(value)) {
|
||||
return this.convertError(moduleName, value);
|
||||
}
|
||||
if (isMetadataSymbolicExpression(value)) {
|
||||
return this.convertExpression(moduleName, value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(v => this.convertValue(moduleName, v));
|
||||
}
|
||||
|
||||
// Otherwise it is a metadata object.
|
||||
const object = value as MetadataObject;
|
||||
const result: MetadataObject = {};
|
||||
for (const key in object) {
|
||||
result[key] = this.convertValue(moduleName, object[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private convertExpression(
|
||||
moduleName: string, value: MetadataSymbolicExpression|MetadataError|
|
||||
undefined): MetadataSymbolicExpression|MetadataError|undefined {
|
||||
if (value) {
|
||||
switch (value.__symbolic) {
|
||||
case 'error':
|
||||
return this.convertError(moduleName, value as MetadataError);
|
||||
case 'reference':
|
||||
return this.convertReference(moduleName, value as MetadataSymbolicReferenceExpression);
|
||||
default:
|
||||
return this.convertExpressionNode(moduleName, value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private convertError(module: string, value: MetadataError): MetadataError {
|
||||
return {
|
||||
__symbolic: 'error',
|
||||
message: value.message,
|
||||
line: value.line,
|
||||
character: value.character,
|
||||
context: value.context, module
|
||||
};
|
||||
}
|
||||
|
||||
private convertReference(moduleName: string, value: MetadataSymbolicReferenceExpression):
|
||||
MetadataSymbolicReferenceExpression|MetadataError {
|
||||
const createReference = (symbol: Symbol): MetadataSymbolicReferenceExpression => {
|
||||
const declaration = symbol.declaration;
|
||||
if (declaration.module.startsWith('.')) {
|
||||
// Reference to a symbol defined in the module. Ensure it is converted then return a
|
||||
// references to the final symbol.
|
||||
this.convertSymbol(symbol);
|
||||
return {
|
||||
__symbolic: 'reference',
|
||||
get name() {
|
||||
// Resolved lazily because private names are assigned late.
|
||||
const canonicalSymbol = symbol.canonicalSymbol;
|
||||
if (canonicalSymbol.isPrivate == null) {
|
||||
throw Error('Invalid state: isPrivate was not initialized');
|
||||
}
|
||||
return canonicalSymbol.isPrivate ? canonicalSymbol.privateName : canonicalSymbol.name;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// The symbol was a re-exported symbol from another module. Return a reference to the
|
||||
// original imported symbol.
|
||||
return {__symbolic: 'reference', name: declaration.name, module: declaration.module};
|
||||
}
|
||||
};
|
||||
|
||||
if (isMetadataGlobalReferenceExpression(value)) {
|
||||
const metadata = this.getMetadata(moduleName);
|
||||
if (metadata && metadata.metadata && metadata.metadata[value.name]) {
|
||||
// Reference to a symbol defined in the module
|
||||
return createReference(this.canonicalSymbolOf(moduleName, value.name));
|
||||
}
|
||||
|
||||
// If a reference has arguments, the arguments need to be converted.
|
||||
if (value.arguments) {
|
||||
return {
|
||||
__symbolic: 'reference',
|
||||
name: value.name,
|
||||
arguments: value.arguments.map(a => this.convertValue(moduleName, a))
|
||||
};
|
||||
}
|
||||
|
||||
// Global references without arguments (such as to Math or JSON) are unmodified.
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isMetadataImportedSymbolReferenceExpression(value)) {
|
||||
// References to imported symbols are separated into two, references to bundled modules and
|
||||
// references to modules
|
||||
// external to the bundle. If the module reference is relative it is assuemd to be in the
|
||||
// bundle. If it is Global
|
||||
// it is assumed to be outside the bundle. References to symbols outside the bundle are left
|
||||
// unmodified. Refernces
|
||||
// to symbol inside the bundle need to be converted to a bundle import reference reachable
|
||||
// from the bundle index.
|
||||
|
||||
if (value.module.startsWith('.')) {
|
||||
// Reference is to a symbol defined inside the module. Convert the reference to a reference
|
||||
// to the canonical
|
||||
// symbol.
|
||||
const referencedModule = resolveModule(value.module, moduleName);
|
||||
const referencedName = value.name;
|
||||
return createReference(this.canonicalSymbolOf(referencedModule, referencedName));
|
||||
}
|
||||
|
||||
// Value is a reference to a symbol defined outside the module.
|
||||
if (value.arguments) {
|
||||
// If a reference has arguments the arguments need to be converted.
|
||||
const result: MetadataImportedSymbolReferenceExpression = {
|
||||
__symbolic: 'reference',
|
||||
name: value.name,
|
||||
module: value.module,
|
||||
arguments: value.arguments.map(a => this.convertValue(moduleName, a))
|
||||
};
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isMetadataModuleReferenceExpression(value)) {
|
||||
// Cannot support references to bundled modules as the internal modules of a bundle are erased
|
||||
// by the bundler.
|
||||
if (value.module.startsWith('.')) {
|
||||
return {
|
||||
__symbolic: 'error',
|
||||
message: 'Unsupported bundled module reference',
|
||||
context: {module: value.module}
|
||||
};
|
||||
}
|
||||
|
||||
// References to unbundled modules are unmodified.
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private convertExpressionNode(moduleName: string, value: MetadataSymbolicExpression):
|
||||
MetadataSymbolicExpression {
|
||||
const result: MetadataSymbolicExpression = {__symbolic: value.__symbolic};
|
||||
for (const key in value) {
|
||||
(result as any)[key] = this.convertValue(moduleName, (value as any)[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private symbolOf(module: string, name: string): Symbol {
|
||||
const symbolKey = `${module}:${name}`;
|
||||
let symbol = this.symbolMap.get(symbolKey);
|
||||
if (!symbol) {
|
||||
symbol = {module, name};
|
||||
this.symbolMap.set(symbolKey, symbol);
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
private canonicalSymbolOf(module: string, name: string): Symbol {
|
||||
const symbol = this.symbolOf(module, name);
|
||||
if (!symbol.canonicalSymbol) {
|
||||
// If we get a symbol after canonical symbols have been assigned it must be a private
|
||||
// symbol so treat it as one.
|
||||
symbol.declaration = symbol;
|
||||
symbol.canonicalSymbol = symbol;
|
||||
symbol.isPrivate = true;
|
||||
this.convertSymbol(symbol);
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
export class CompilerHostAdapter implements MetadataBundlerHost {
|
||||
private collector = new MetadataCollector();
|
||||
|
||||
constructor(private host: ts.CompilerHost) {}
|
||||
|
||||
getMetadataFor(fileName: string): ModuleMetadata {
|
||||
const sourceFile = this.host.getSourceFile(fileName + '.ts', ts.ScriptTarget.Latest);
|
||||
return this.collector.getMetadata(sourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveModule(importName: string, from: string): string {
|
||||
if (importName.startsWith('.') && from) {
|
||||
return normalize(path.join(path.dirname(from), importName));
|
||||
}
|
||||
return importName;
|
||||
}
|
||||
|
||||
function normalize(path: string): string {
|
||||
const parts = path.split('/');
|
||||
const segments: string[] = [];
|
||||
for (const part of parts) {
|
||||
switch (part) {
|
||||
case '':
|
||||
case '.':
|
||||
continue;
|
||||
case '..':
|
||||
segments.pop();
|
||||
break;
|
||||
default:
|
||||
segments.push(part);
|
||||
}
|
||||
}
|
||||
if (segments.length) {
|
||||
segments.unshift(path.startsWith('/') ? '' : '.');
|
||||
return segments.join('/');
|
||||
} else {
|
||||
return '.';
|
||||
}
|
||||
}
|
||||
|
||||
function isPrimitive(o: any): o is boolean|string|number {
|
||||
return o === null || (typeof o !== 'function' && typeof o !== 'object');
|
||||
}
|
||||
|
||||
function isMetadataArray(o: MetadataValue): o is MetadataArray {
|
||||
return Array.isArray(o);
|
||||
}
|
||||
|
||||
function getRootExport(symbol: Symbol): Symbol {
|
||||
return symbol.reexportedAs ? getRootExport(symbol.reexportedAs) : symbol;
|
||||
}
|
||||
|
||||
function getSymbolDeclaration(symbol: Symbol): Symbol {
|
||||
return symbol.exports ? getSymbolDeclaration(symbol.exports) : symbol;
|
||||
}
|
|
@ -51,6 +51,7 @@ export abstract class DelegatingHost implements ts.CompilerHost {
|
|||
}
|
||||
|
||||
const IGNORED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$/;
|
||||
const DTS = /\.d\.ts$/;
|
||||
|
||||
export class MetadataWriterHost extends DelegatingHost {
|
||||
private metadataCollector = new MetadataCollector({quotedNames: true});
|
||||
|
@ -108,6 +109,48 @@ export class MetadataWriterHost extends DelegatingHost {
|
|||
if (sourceFiles.length > 1) {
|
||||
throw new Error('Bundled emit with --out is not supported');
|
||||
}
|
||||
this.writeMetadata(fileName, sourceFiles[0]);
|
||||
if (!this.ngOptions.skipMetadataEmit && !this.ngOptions.bundleIndex) {
|
||||
this.writeMetadata(fileName, sourceFiles[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SyntheticIndexHost extends DelegatingHost {
|
||||
constructor(
|
||||
delegate: ts.CompilerHost,
|
||||
private syntheticIndex: {name: string, content: string, metadata: string}) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
fileExists = (fileName: string):
|
||||
boolean => {
|
||||
return fileName == this.syntheticIndex.name || this.delegate.fileExists(fileName);
|
||||
}
|
||||
|
||||
readFile =
|
||||
(fileName: string) => {
|
||||
return fileName == this.syntheticIndex.name ? this.syntheticIndex.content :
|
||||
this.delegate.readFile(fileName);
|
||||
}
|
||||
|
||||
getSourceFile =
|
||||
(fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: (message: string) => void) => {
|
||||
if (fileName == this.syntheticIndex.name) {
|
||||
return ts.createSourceFile(fileName, this.syntheticIndex.content, languageVersion, true);
|
||||
}
|
||||
return this.delegate.getSourceFile(fileName, languageVersion, onError);
|
||||
}
|
||||
|
||||
writeFile: ts.WriteFileCallback =
|
||||
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
||||
this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||
if (fileName.match(DTS) && sourceFiles && sourceFiles.length == 1 &&
|
||||
sourceFiles[0].fileName == this.syntheticIndex.name) {
|
||||
// If we are writing the synthetic index, write the metadata along side.
|
||||
const metadataName = fileName.replace(DTS, '.metadata.json');
|
||||
writeFileSync(metadataName, this.syntheticIndex.metadata, 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* @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 {BundlePrivateEntry} from './bundler';
|
||||
|
||||
const INDEX_HEADER = `/**
|
||||
* Generated bundle index. Do not edit.
|
||||
*/
|
||||
`;
|
||||
|
||||
type MapEntry = [string, BundlePrivateEntry[]];
|
||||
|
||||
export function privateEntriesToIndex(index: string, privates: BundlePrivateEntry[]): string {
|
||||
const results: string[] = [INDEX_HEADER];
|
||||
|
||||
// Export all of the index symbols.
|
||||
results.push(`export * from '${index}';`, '');
|
||||
|
||||
// Simplify the exports
|
||||
const exports = new Map<string, BundlePrivateEntry[]>();
|
||||
|
||||
for (const entry of privates) {
|
||||
let entries = exports.get(entry.module);
|
||||
if (!entries) {
|
||||
entries = [];
|
||||
exports.set(entry.module, entries);
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
|
||||
const compareEntries = compare((e: BundlePrivateEntry) => e.name);
|
||||
const compareModules = compare((e: MapEntry) => e[0]);
|
||||
const orderedExports =
|
||||
Array.from(exports)
|
||||
.map(([module, entries]) => <MapEntry>[module, entries.sort(compareEntries)])
|
||||
.sort(compareModules);
|
||||
|
||||
for (const [module, entries] of orderedExports) {
|
||||
let symbols = entries.map(e => `${e.name} as ${e.privateName}`);
|
||||
results.push(`export {${symbols}} from '${module}';`);
|
||||
}
|
||||
|
||||
return results.join('\n');
|
||||
}
|
||||
|
||||
function compare<E, T>(select: (e: E) => T): (a: E, b: E) => number {
|
||||
return (a, b) => {
|
||||
const ak = select(a);
|
||||
const bk = select(b);
|
||||
return ak > bk ? 1 : ak < bk ? -1 : 0;
|
||||
};
|
||||
}
|
|
@ -14,12 +14,15 @@ import * as ts from 'typescript';
|
|||
import {check, tsc} from './tsc';
|
||||
|
||||
import NgOptions from './options';
|
||||
import {MetadataWriterHost} from './compiler_host';
|
||||
import {MetadataWriterHost, SyntheticIndexHost} from './compiler_host';
|
||||
import {CliOptions} from './cli_options';
|
||||
import {VinylFile, isVinylFile} from './vinyl_file';
|
||||
|
||||
import {MetadataBundler, CompilerHostAdapter} from './bundler';
|
||||
import {privateEntriesToIndex} from './index_writer';
|
||||
export {UserError} from './tsc';
|
||||
|
||||
const DTS = /\.d\.ts$/;
|
||||
|
||||
export type CodegenExtension =
|
||||
(ngOptions: NgOptions, cliOptions: CliOptions, program: ts.Program, host: ts.CompilerHost) =>
|
||||
Promise<void>;
|
||||
|
@ -49,13 +52,46 @@ export function main(
|
|||
const diagnostics = (parsed.options as any).diagnostics;
|
||||
if (diagnostics) (ts as any).performance.enable();
|
||||
|
||||
const host = ts.createCompilerHost(parsed.options, true);
|
||||
let host = ts.createCompilerHost(parsed.options, true);
|
||||
|
||||
// HACK: patch the realpath to solve symlink issue here:
|
||||
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||
// todo(misko): remove once facade symlinks are removed
|
||||
host.realpath = (path) => path;
|
||||
|
||||
// If the comilation is a bundle index then produce the bundle index metadata and
|
||||
// the synthetic bundle index.
|
||||
if (ngOptions.bundleIndex && !ngOptions.skipMetadataEmit) {
|
||||
const files = parsed.fileNames.filter(f => !DTS.test(f));
|
||||
if (files.length != 1 && (!ngOptions.libraryIndex || files.length < 1)) {
|
||||
check([{
|
||||
file: null,
|
||||
start: null,
|
||||
length: null,
|
||||
messageText:
|
||||
'Angular compiler option "bundleIndex" requires one and only one .ts file in the "files" field or "libraryIndex" to also be specified in order to select which module to use as the library index',
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0
|
||||
}]);
|
||||
}
|
||||
const file = files[0];
|
||||
const indexModule = file.replace(/\.ts$/, '');
|
||||
const libraryIndexModule = ngOptions.libraryIndex ?
|
||||
MetadataBundler.resolveModule(ngOptions.libraryIndex, indexModule) :
|
||||
indexModule;
|
||||
const bundler =
|
||||
new MetadataBundler(indexModule, ngOptions.importAs, new CompilerHostAdapter(host));
|
||||
if (diagnostics) console.time('NG bundle index');
|
||||
const metadataBundle = bundler.getMetadataBundle();
|
||||
if (diagnostics) console.timeEnd('NG bundle index');
|
||||
const metadata = JSON.stringify(metadataBundle.metadata);
|
||||
const name = path.join(path.dirname(libraryIndexModule), ngOptions.bundleIndex + '.ts');
|
||||
const libraryIndex = ngOptions.libraryIndex || `./${path.basename(indexModule)}`;
|
||||
const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates);
|
||||
host = new SyntheticIndexHost(host, {name, content, metadata});
|
||||
parsed.fileNames.push(name);
|
||||
}
|
||||
|
||||
const program = createProgram(host);
|
||||
const errors = program.getOptionsDiagnostics();
|
||||
check(errors);
|
||||
|
|
|
@ -25,6 +25,41 @@ interface Options extends ts.CompilerOptions {
|
|||
// Don't produce .ngfactory.ts or .ngstyle.ts files
|
||||
skipTemplateCodegen?: boolean;
|
||||
|
||||
// Whether to generate a bundle index of the given name and the corresponding bundled
|
||||
// metadata. This option is intended to be used when creating library bundles similar
|
||||
// to how `@angular/core` and `@angular/common` are generated.
|
||||
// When this option is used the `package.json` for the library should refered to the
|
||||
// generated bundle index instead of the library index file. Only the bundle index
|
||||
// metadata is required as the bundle index contains all metadata visible from the
|
||||
// bundle index. The bundle index is used to import symbols for generating
|
||||
// .ngfactory.ts files and includes both the public API from the root .ts file as well
|
||||
// as shrowded internal symbols.
|
||||
// The by default the .ts file supplied in the `files` files field is assumed to be
|
||||
// library index. If more than one is specified, uses `libraryIndex` to select the
|
||||
// file to use. If more than on .ts file is supplied and no `libraryIndex` is supllied
|
||||
// an error is produced.
|
||||
// A bundle index .d.ts and .js will be created with the given `bundleIndex` name in the
|
||||
// same location as the library index .d.ts file is emitted.
|
||||
// For example, if a library uses `index.ts` file as the root file, the `tsconfig.json`
|
||||
// `files` field would be `["index.ts"]`. The `bundleIndex` options could then be set
|
||||
// to, for example `"bundle_index"`, which produces a `bundle_index.d.ts` and
|
||||
// `bundle_index.metadata.json` files. The library's `package.json`'s `module` field
|
||||
// would be `"bundle_index.js"` and the `typings` field would be `"bundle_index.d.ts"`.
|
||||
bundleIndex?: string;
|
||||
|
||||
// Override which module is used as the library index. This is only meaningful if
|
||||
// `bundleIndex` is also supplied and only necessary if more than one `.ts` file is
|
||||
// supplied in the `files` field. This must be of the form found in a import
|
||||
// declaration. For example, if the library index is in `index.ts` then the
|
||||
// `libraryIndex` field should be `"./index"`.
|
||||
libraryIndex?: string;
|
||||
|
||||
// Preferred module name to use for importing the generated bundle. References
|
||||
// generated by `ngc` will use this module name when importing symbols from the
|
||||
// generated bundle. This is only meaningful when `bundleIndex` is also supplied. It is
|
||||
// otherwise ignored.
|
||||
importAs?: string;
|
||||
|
||||
// Whether to generate code for library code.
|
||||
// If true, produce .ngfactory.ts and .ngstyle.ts files for .d.ts inputs.
|
||||
// Default is true.
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface ModuleMetadata {
|
|||
__symbolic: 'module';
|
||||
version: number;
|
||||
exports?: ModuleExportMetadata[];
|
||||
importAs?: string;
|
||||
metadata: {[name: string]: MetadataEntry};
|
||||
}
|
||||
export function isModuleMetadata(value: any): value is ModuleMetadata {
|
||||
|
@ -261,6 +262,11 @@ export interface MetadataError {
|
|||
*/
|
||||
character?: number;
|
||||
|
||||
/**
|
||||
* The module of the error (only used in bundled metadata)
|
||||
*/
|
||||
module?: string;
|
||||
|
||||
/**
|
||||
* Context information that can be used to generate a more descriptive error message. The content
|
||||
* of the context is dependent on the error message.
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* @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 * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {MetadataBundler, MetadataBundlerHost} from '../src/bundler';
|
||||
import {MetadataCollector} from '../src/collector';
|
||||
import {ModuleMetadata} from '../src/schema';
|
||||
|
||||
import {Directory, open} from './typescript.mocks';
|
||||
|
||||
describe('metadata bundler', () => {
|
||||
|
||||
it('should be able to bundle a simple library', () => {
|
||||
const host = new MockStringBundlerHost('/', SIMPLE_LIBRARY);
|
||||
const bundler = new MetadataBundler('/lib/index', undefined, host);
|
||||
const result = bundler.getMetadataBundle();
|
||||
expect(Object.keys(result.metadata.metadata).sort()).toEqual([
|
||||
'ONE_CLASSES', 'One', 'OneMore', 'TWO_CLASSES', 'Two', 'TwoMore', 'ɵa', 'ɵb'
|
||||
]);
|
||||
expect(result.privates).toEqual([
|
||||
{privateName: 'ɵa', name: 'PrivateOne', module: './src/one'},
|
||||
{privateName: 'ɵb', name: 'PrivateTwo', module: './src/two/index'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to bundle an oddly constructed library', () => {
|
||||
const host = new MockStringBundlerHost('/', {
|
||||
'lib': {
|
||||
'index.ts': `
|
||||
export * from './src/index';
|
||||
`,
|
||||
'src': {
|
||||
'index.ts': `
|
||||
export {One, OneMore, ONE_CLASSES} from './one';
|
||||
export {Two, TwoMore, TWO_CLASSES} from './two/index';
|
||||
`,
|
||||
'one.ts': `
|
||||
class One {}
|
||||
class OneMore extends One {}
|
||||
class PrivateOne {}
|
||||
const ONE_CLASSES = [One, OneMore, PrivateOne];
|
||||
export {One, OneMore, PrivateOne, ONE_CLASSES};
|
||||
`,
|
||||
'two': {
|
||||
'index.ts': `
|
||||
class Two {}
|
||||
class TwoMore extends Two {}
|
||||
class PrivateTwo {}
|
||||
const TWO_CLASSES = [Two, TwoMore, PrivateTwo];
|
||||
export {Two, TwoMore, PrivateTwo, TWO_CLASSES};
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const bundler = new MetadataBundler('/lib/index', undefined, host);
|
||||
const result = bundler.getMetadataBundle();
|
||||
expect(Object.keys(result.metadata.metadata).sort()).toEqual([
|
||||
'ONE_CLASSES', 'One', 'OneMore', 'TWO_CLASSES', 'Two', 'TwoMore', 'ɵa', 'ɵb'
|
||||
]);
|
||||
expect(result.privates).toEqual([
|
||||
{privateName: 'ɵa', name: 'PrivateOne', module: './src/one'},
|
||||
{privateName: 'ɵb', name: 'PrivateTwo', module: './src/two/index'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert re-exported to the export', () => {
|
||||
const host = new MockStringBundlerHost('/', {
|
||||
'index.ts': `
|
||||
export * from './bar';
|
||||
export * from './foo';
|
||||
`,
|
||||
'bar.ts': `
|
||||
import {Foo} from './foo';
|
||||
export class Bar extends Foo {
|
||||
|
||||
}
|
||||
`,
|
||||
'foo.ts': `
|
||||
export {Foo} from 'foo';
|
||||
`
|
||||
});
|
||||
const bundler = new MetadataBundler('/index', undefined, host);
|
||||
const result = bundler.getMetadataBundle();
|
||||
// Expect the extends reference to refer to the imported module
|
||||
expect((result.metadata.metadata as any).Bar.extends.module).toEqual('foo');
|
||||
expect(result.privates).toEqual([]);
|
||||
});
|
||||
|
||||
it('should treat import then export as a simple export', () => {
|
||||
const host = new MockStringBundlerHost('/', {
|
||||
'index.ts': `
|
||||
export * from './a';
|
||||
export * from './c';
|
||||
`,
|
||||
'a.ts': `
|
||||
import { B } from './b';
|
||||
export { B };
|
||||
`,
|
||||
'b.ts': `
|
||||
export class B { }
|
||||
`,
|
||||
'c.ts': `
|
||||
import { B } from './b';
|
||||
export class C extends B { }
|
||||
`
|
||||
});
|
||||
const bundler = new MetadataBundler('/index', undefined, host);
|
||||
const result = bundler.getMetadataBundle();
|
||||
expect(Object.keys(result.metadata.metadata).sort()).toEqual(['B', 'C']);
|
||||
expect(result.privates).toEqual([]);
|
||||
});
|
||||
|
||||
it('should be able to bundle a private from a un-exported module', () => {
|
||||
const host = new MockStringBundlerHost('/', {
|
||||
'index.ts': `
|
||||
export * from './foo';
|
||||
`,
|
||||
'foo.ts': `
|
||||
import {Bar} from './bar';
|
||||
export class Foo extends Bar {
|
||||
|
||||
}
|
||||
`,
|
||||
'bar.ts': `
|
||||
export class Bar {}
|
||||
`
|
||||
});
|
||||
const bundler = new MetadataBundler('/index', undefined, host);
|
||||
const result = bundler.getMetadataBundle();
|
||||
expect(Object.keys(result.metadata.metadata).sort()).toEqual(['Foo', 'ɵa']);
|
||||
expect(result.privates).toEqual([{privateName: 'ɵa', name: 'Bar', module: './bar'}]);
|
||||
});
|
||||
});
|
||||
|
||||
export class MockStringBundlerHost implements MetadataBundlerHost {
|
||||
collector = new MetadataCollector();
|
||||
|
||||
constructor(private dirName: string, private directory: Directory) {}
|
||||
|
||||
getMetadataFor(moduleName: string): ModuleMetadata {
|
||||
const fileName = path.join(this.dirName, moduleName) + '.ts';
|
||||
const text = open(this.directory, fileName);
|
||||
if (typeof text == 'string') {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
fileName, text, ts.ScriptTarget.Latest, /* setParent */ true, ts.ScriptKind.TS);
|
||||
const diagnostics: ts.Diagnostic[] = (sourceFile as any).parseDiagnostics;
|
||||
if (diagnostics && diagnostics.length) {
|
||||
throw Error('Unexpected syntax error in test');
|
||||
}
|
||||
const result = this.collector.getMetadata(sourceFile);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const SIMPLE_LIBRARY = {
|
||||
'lib': {
|
||||
'index.ts': `
|
||||
export * from './src/index';
|
||||
`,
|
||||
'src': {
|
||||
'index.ts': `
|
||||
export {One, OneMore, ONE_CLASSES} from './one';
|
||||
export {Two, TwoMore, TWO_CLASSES} from './two/index';
|
||||
`,
|
||||
'one.ts': `
|
||||
export class One {}
|
||||
export class OneMore extends One {}
|
||||
export class PrivateOne {}
|
||||
export const ONE_CLASSES = [One, OneMore, PrivateOne];
|
||||
`,
|
||||
'two': {
|
||||
'index.ts': `
|
||||
export class Two {}
|
||||
export class TwoMore extends Two {}
|
||||
export class PrivateTwo {}
|
||||
export const TWO_CLASSES = [Two, TwoMore, PrivateTwo];
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @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 {MetadataBundler} from '../src/bundler';
|
||||
import {MetadataCollector} from '../src/collector';
|
||||
import {privateEntriesToIndex} from '../src/index_writer';
|
||||
|
||||
import {MockStringBundlerHost, SIMPLE_LIBRARY} from './bundler_spec';
|
||||
|
||||
describe('index_writer', () => {
|
||||
it('should be able to write the index of a simple library', () => {
|
||||
const host = new MockStringBundlerHost('/', SIMPLE_LIBRARY);
|
||||
const bundler = new MetadataBundler('/lib/index', undefined, host);
|
||||
const bundle = bundler.getMetadataBundle();
|
||||
const result = privateEntriesToIndex('./index', bundle.privates);
|
||||
expect(result).toContain(`export * from './index';`);
|
||||
expect(result).toContain(`export {PrivateOne as ɵa} from './src/one';`);
|
||||
expect(result).toContain(`export {PrivateTwo as ɵb} from './src/two/index';`);
|
||||
});
|
||||
});
|
|
@ -45,20 +45,25 @@ export class Host implements ts.LanguageServiceHost {
|
|||
if (this.overrides.has(fileName)) {
|
||||
return this.overrides.get(fileName);
|
||||
}
|
||||
const names = fileName.split('/');
|
||||
if (names[names.length - 1] === 'lib.d.ts') {
|
||||
if (fileName.endsWith('lib.d.ts')) {
|
||||
return fs.readFileSync(ts.getDefaultLibFilePath(this.getCompilationSettings()), 'utf8');
|
||||
}
|
||||
let current: Directory|string = this.directory;
|
||||
if (names.length && names[0] === '') names.shift();
|
||||
for (const name of names) {
|
||||
if (!current || typeof current === 'string') return undefined;
|
||||
current = (<any>current)[name];
|
||||
}
|
||||
const current = open(this.directory, fileName);
|
||||
if (typeof current === 'string') return current;
|
||||
}
|
||||
}
|
||||
|
||||
export function open(directory: Directory, fileName: string): Directory|string|undefined {
|
||||
const names = fileName.split('/');
|
||||
let current: Directory|string = directory;
|
||||
if (names.length && names[0] === '') names.shift();
|
||||
for (const name of names) {
|
||||
if (!current || typeof current === 'string') return undefined;
|
||||
current = current[name];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export class MockNode implements ts.Node {
|
||||
constructor(
|
||||
public kind: ts.SyntaxKind = ts.SyntaxKind.Identifier, public flags: ts.NodeFlags = 0,
|
||||
|
|
Loading…
Reference in New Issue