refactor(tsc-wrapped): move tsc-wrapped to the packages directory (#18160)
This commit is contained in:

committed by
Miško Hevery

parent
a6c635e69e
commit
619e625ee2
597
packages/tsc-wrapped/src/bundler.ts
Normal file
597
packages/tsc-wrapped/src/bundler.ts
Normal file
@ -0,0 +1,597 @@
|
||||
/**
|
||||
* @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, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from './schema';
|
||||
|
||||
|
||||
|
||||
// The character set used to produce private names.
|
||||
const PRIVATE_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
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;
|
||||
|
||||
// A symbol is marked as a re-export the symbol was rexported from a module that is
|
||||
// not part of the flat module bundle.
|
||||
reexport?: 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;
|
||||
private exported: Set<Symbol>;
|
||||
|
||||
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 metadata = 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
|
||||
}));
|
||||
const origins = Array.from(this.symbolMap.values())
|
||||
.filter(s => s.referenced && !s.reexport)
|
||||
.reduce<{[name: string]: string}>((p, s) => {
|
||||
p[s.isPrivate ? s.privateName : s.name] = s.declaration.module;
|
||||
return p;
|
||||
}, {});
|
||||
const exports = this.getReExports(exportedSymbols);
|
||||
return {
|
||||
metadata: {
|
||||
__symbolic: 'module',
|
||||
version: VERSION,
|
||||
exports: exports.length ? exports : undefined, metadata, origins,
|
||||
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.
|
||||
const exportedSymbols = 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;
|
||||
const symbol = this.symbolOf(exportFrom, name);
|
||||
if (exportedSymbols && exportedSymbols.length == 1 && exportedSymbols[0].reexport &&
|
||||
exportedSymbols[0].name == '*') {
|
||||
// This is a named export from a module we have no metadata about. Record the named
|
||||
// export as a re-export.
|
||||
symbol.reexport = true;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!module) {
|
||||
// If no metadata is found for this import then it is considered external to the
|
||||
// library and should be recorded as a re-export in the final metadata if it is
|
||||
// eventually re-exported.
|
||||
const symbol = this.symbolOf(moduleName, '*');
|
||||
symbol.reexport = true;
|
||||
result.push(symbol);
|
||||
}
|
||||
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());
|
||||
this.exported = new Set(exportedSymbols);
|
||||
symbols.forEach(this.canonicalizeSymbol, this);
|
||||
}
|
||||
|
||||
private canonicalizeSymbol(symbol: Symbol) {
|
||||
const rootExport = getRootExport(symbol);
|
||||
const declaration = getSymbolDeclaration(symbol);
|
||||
const isPrivate = !this.exported.has(rootExport);
|
||||
const canonicalSymbol = isPrivate ? declaration : rootExport;
|
||||
symbol.isPrivate = isPrivate;
|
||||
symbol.declaration = declaration;
|
||||
symbol.canonicalSymbol = canonicalSymbol;
|
||||
symbol.reexport = declaration.reexport;
|
||||
}
|
||||
|
||||
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 && !symbol.reexport) {
|
||||
let name = symbol.name;
|
||||
if (symbol.isPrivate && !symbol.privateName) {
|
||||
name = newPrivateName();
|
||||
symbol.privateName = name;
|
||||
}
|
||||
result[name] = symbol.value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getReExports(exportedSymbols: Symbol[]): ModuleExportMetadata[] {
|
||||
type ExportClause = {name: string, as: string}[];
|
||||
const modules = new Map<string, ExportClause>();
|
||||
const exportAlls = new Set<string>();
|
||||
for (const symbol of exportedSymbols) {
|
||||
if (symbol.reexport) {
|
||||
const declaration = symbol.declaration;
|
||||
const module = declaration.module;
|
||||
if (declaration.name == '*') {
|
||||
// Reexport all the symbols.
|
||||
exportAlls.add(declaration.module);
|
||||
} else {
|
||||
// Re-export the symbol as the exported name.
|
||||
let entry = modules.get(module);
|
||||
if (!entry) {
|
||||
entry = [];
|
||||
modules.set(module, entry);
|
||||
}
|
||||
const as = symbol.name;
|
||||
const name = declaration.name;
|
||||
entry.push({name, as});
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
...Array.from(exportAlls.values()).map(from => ({from})),
|
||||
...Array.from(modules.entries()).map(([from, exports]) => ({export: exports, from}))
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (isInterfaceMetadata(value)) {
|
||||
return 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
|
||||
// assumed 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. References 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.
|
||||
return {
|
||||
__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 {
|
||||
// Ensure the module has been seen.
|
||||
this.exportAll(module);
|
||||
const symbol = this.symbolOf(module, name);
|
||||
if (!symbol.canonicalSymbol) {
|
||||
this.canonicalizeSymbol(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) {
|
||||
let normalPath = path.normalize(path.join(path.dirname(from), importName));
|
||||
if (!normalPath.startsWith('.') && from.startsWith('.')) {
|
||||
// path.normalize() preserves leading '../' but not './'. This adds it back.
|
||||
normalPath = `.${path.sep}${normalPath}`;
|
||||
}
|
||||
// Replace windows path delimiters with forward-slashes. Otherwise the paths are not
|
||||
// TypeScript compatible when building the bundle.
|
||||
return normalPath.replace(/\\/g, '/');
|
||||
}
|
||||
return importName;
|
||||
}
|
||||
|
||||
function isPrimitive(o: any): o is boolean|string|number {
|
||||
return o === null || (typeof o !== 'function' && typeof o !== 'object');
|
||||
}
|
||||
|
||||
function getRootExport(symbol: Symbol): Symbol {
|
||||
return symbol.reexportedAs ? getRootExport(symbol.reexportedAs) : symbol;
|
||||
}
|
||||
|
||||
function getSymbolDeclaration(symbol: Symbol): Symbol {
|
||||
return symbol.exports ? getSymbolDeclaration(symbol.exports) : symbol;
|
||||
}
|
50
packages/tsc-wrapped/src/cli_options.ts
Normal file
50
packages/tsc-wrapped/src/cli_options.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
export class CliOptions {
|
||||
public basePath: string;
|
||||
constructor({basePath = null}: {basePath?: string}) { this.basePath = basePath; }
|
||||
}
|
||||
|
||||
export class I18nExtractionCliOptions extends CliOptions {
|
||||
i18nFormat: string|null;
|
||||
locale: string|null;
|
||||
outFile: string|null;
|
||||
|
||||
constructor({i18nFormat = null, locale = null, outFile = null}: {
|
||||
i18nFormat?: string,
|
||||
locale?: string,
|
||||
outFile?: string,
|
||||
}) {
|
||||
super({});
|
||||
this.i18nFormat = i18nFormat;
|
||||
this.locale = locale;
|
||||
this.outFile = outFile;
|
||||
}
|
||||
}
|
||||
|
||||
export class NgcCliOptions extends CliOptions {
|
||||
public i18nFormat: string;
|
||||
public i18nFile: string;
|
||||
public locale: string;
|
||||
public missingTranslation: string;
|
||||
|
||||
constructor({i18nFormat = null, i18nFile = null, locale = null, missingTranslation = null,
|
||||
basePath = null}: {
|
||||
i18nFormat?: string,
|
||||
i18nFile?: string,
|
||||
locale?: string,
|
||||
missingTranslation?: string,
|
||||
basePath?: string
|
||||
}) {
|
||||
super({basePath: basePath});
|
||||
this.i18nFormat = i18nFormat;
|
||||
this.i18nFile = i18nFile;
|
||||
this.locale = locale;
|
||||
this.missingTranslation = missingTranslation;
|
||||
}
|
||||
}
|
727
packages/tsc-wrapped/src/collector.ts
Normal file
727
packages/tsc-wrapped/src/collector.ts
Normal file
@ -0,0 +1,727 @@
|
||||
/**
|
||||
* @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 ts from 'typescript';
|
||||
|
||||
import {Evaluator, errorSymbol} from './evaluator';
|
||||
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, InterfaceMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
// In TypeScript 2.1 these flags moved
|
||||
// These helpers work for both 2.0 and 2.1.
|
||||
const isExport = (ts as any).ModifierFlags ?
|
||||
((node: ts.Node) =>
|
||||
!!((ts as any).getCombinedModifierFlags(node) & (ts as any).ModifierFlags.Export)) :
|
||||
((node: ts.Node) => !!((node.flags & (ts as any).NodeFlags.Export)));
|
||||
const isStatic = (ts as any).ModifierFlags ?
|
||||
((node: ts.Node) =>
|
||||
!!((ts as any).getCombinedModifierFlags(node) & (ts as any).ModifierFlags.Static)) :
|
||||
((node: ts.Node) => !!((node.flags & (ts as any).NodeFlags.Static)));
|
||||
|
||||
/**
|
||||
* A set of collector options to use when collecting metadata.
|
||||
*/
|
||||
export class CollectorOptions {
|
||||
/**
|
||||
* Version of the metadata to collect.
|
||||
*/
|
||||
version?: number;
|
||||
|
||||
/**
|
||||
* Collect a hidden field "$quoted$" in objects literals that record when the key was quoted in
|
||||
* the source.
|
||||
*/
|
||||
quotedNames?: boolean;
|
||||
|
||||
/**
|
||||
* Do not simplify invalid expressions.
|
||||
*/
|
||||
verboseInvalidExpression?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect decorator metadata from a TypeScript module.
|
||||
*/
|
||||
export class MetadataCollector {
|
||||
constructor(private options: CollectorOptions = {}) {}
|
||||
|
||||
/**
|
||||
* Returns a JSON.stringify friendly form describing the decorators of the exported classes from
|
||||
* the source file that is expected to correspond to a module.
|
||||
*/
|
||||
public getMetadata(sourceFile: ts.SourceFile, strict: boolean = false): ModuleMetadata {
|
||||
const locals = new Symbols(sourceFile);
|
||||
const nodeMap =
|
||||
new Map<MetadataValue|ClassMetadata|InterfaceMetadata|FunctionMetadata, ts.Node>();
|
||||
const evaluator = new Evaluator(locals, nodeMap, this.options);
|
||||
let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined;
|
||||
let exports: ModuleExportMetadata[];
|
||||
|
||||
function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression {
|
||||
return <MetadataSymbolicExpression>evaluator.evaluateNode(decoratorNode.expression);
|
||||
}
|
||||
|
||||
function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T {
|
||||
nodeMap.set(entry, node);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function errorSym(
|
||||
message: string, node?: ts.Node, context?: {[name: string]: string}): MetadataError {
|
||||
return errorSymbol(message, node, context, sourceFile);
|
||||
}
|
||||
|
||||
function maybeGetSimpleFunction(
|
||||
functionDeclaration: ts.FunctionDeclaration |
|
||||
ts.MethodDeclaration): {func: FunctionMetadata, name: string}|undefined {
|
||||
if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
|
||||
const nameNode = <ts.Identifier>functionDeclaration.name;
|
||||
const functionName = nameNode.text;
|
||||
const functionBody = functionDeclaration.body;
|
||||
if (functionBody && functionBody.statements.length == 1) {
|
||||
const statement = functionBody.statements[0];
|
||||
if (statement.kind === ts.SyntaxKind.ReturnStatement) {
|
||||
const returnStatement = <ts.ReturnStatement>statement;
|
||||
if (returnStatement.expression) {
|
||||
const func: FunctionMetadata = {
|
||||
__symbolic: 'function',
|
||||
parameters: namesOf(functionDeclaration.parameters),
|
||||
value: evaluator.evaluateNode(returnStatement.expression)
|
||||
};
|
||||
if (functionDeclaration.parameters.some(p => p.initializer != null)) {
|
||||
func.defaults = functionDeclaration.parameters.map(
|
||||
p => p.initializer && evaluator.evaluateNode(p.initializer));
|
||||
}
|
||||
return recordEntry({func, name: functionName}, functionDeclaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
|
||||
const result: ClassMetadata = {__symbolic: 'class'};
|
||||
|
||||
function getDecorators(decorators: ts.Decorator[]): MetadataSymbolicExpression[] {
|
||||
if (decorators && decorators.length)
|
||||
return decorators.map(decorator => objFromDecorator(decorator));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function referenceFrom(node: ts.Node): MetadataSymbolicReferenceExpression|MetadataError|
|
||||
MetadataSymbolicSelectExpression {
|
||||
const result = evaluator.evaluateNode(node);
|
||||
if (isMetadataError(result) || isMetadataSymbolicReferenceExpression(result) ||
|
||||
isMetadataSymbolicSelectExpression(result)) {
|
||||
return result;
|
||||
} else {
|
||||
return errorSym('Symbol reference expected', node);
|
||||
}
|
||||
}
|
||||
|
||||
// Add class parents
|
||||
if (classDeclaration.heritageClauses) {
|
||||
classDeclaration.heritageClauses.forEach((hc) => {
|
||||
if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) {
|
||||
hc.types.forEach(type => result.extends = referenceFrom(type.expression));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add arity if the type is generic
|
||||
const typeParameters = classDeclaration.typeParameters;
|
||||
if (typeParameters && typeParameters.length) {
|
||||
result.arity = typeParameters.length;
|
||||
}
|
||||
|
||||
// Add class decorators
|
||||
if (classDeclaration.decorators) {
|
||||
result.decorators = getDecorators(classDeclaration.decorators);
|
||||
}
|
||||
|
||||
// member decorators
|
||||
let members: MetadataMap = null;
|
||||
function recordMember(name: string, metadata: MemberMetadata) {
|
||||
if (!members) members = {};
|
||||
const data = members.hasOwnProperty(name) ? members[name] : [];
|
||||
data.push(metadata);
|
||||
members[name] = data;
|
||||
}
|
||||
|
||||
// static member
|
||||
let statics: {[name: string]: MetadataValue | FunctionMetadata} = null;
|
||||
function recordStaticMember(name: string, value: MetadataValue | FunctionMetadata) {
|
||||
if (!statics) statics = {};
|
||||
statics[name] = value;
|
||||
}
|
||||
|
||||
for (const member of classDeclaration.members) {
|
||||
let isConstructor = false;
|
||||
switch (member.kind) {
|
||||
case ts.SyntaxKind.Constructor:
|
||||
case ts.SyntaxKind.MethodDeclaration:
|
||||
isConstructor = member.kind === ts.SyntaxKind.Constructor;
|
||||
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
|
||||
if (isStatic(method)) {
|
||||
const maybeFunc = maybeGetSimpleFunction(<ts.MethodDeclaration>method);
|
||||
if (maybeFunc) {
|
||||
recordStaticMember(maybeFunc.name, maybeFunc.func);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const methodDecorators = getDecorators(method.decorators);
|
||||
const parameters = method.parameters;
|
||||
const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
|
||||
const parametersData:
|
||||
(MetadataSymbolicReferenceExpression | MetadataError |
|
||||
MetadataSymbolicSelectExpression | null)[] = [];
|
||||
let hasDecoratorData: boolean = false;
|
||||
let hasParameterData: boolean = false;
|
||||
for (const parameter of parameters) {
|
||||
const parameterData = getDecorators(parameter.decorators);
|
||||
parameterDecoratorData.push(parameterData);
|
||||
hasDecoratorData = hasDecoratorData || !!parameterData;
|
||||
if (isConstructor) {
|
||||
if (parameter.type) {
|
||||
parametersData.push(referenceFrom(parameter.type));
|
||||
} else {
|
||||
parametersData.push(null);
|
||||
}
|
||||
hasParameterData = true;
|
||||
}
|
||||
}
|
||||
const data: MethodMetadata = {__symbolic: isConstructor ? 'constructor' : 'method'};
|
||||
const name = isConstructor ? '__ctor__' : evaluator.nameOf(member.name);
|
||||
if (methodDecorators) {
|
||||
data.decorators = methodDecorators;
|
||||
}
|
||||
if (hasDecoratorData) {
|
||||
data.parameterDecorators = parameterDecoratorData;
|
||||
}
|
||||
if (hasParameterData) {
|
||||
(<ConstructorMetadata>data).parameters = parametersData;
|
||||
}
|
||||
if (!isMetadataError(name)) {
|
||||
recordMember(name, data);
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.PropertyDeclaration:
|
||||
case ts.SyntaxKind.GetAccessor:
|
||||
case ts.SyntaxKind.SetAccessor:
|
||||
const property = <ts.PropertyDeclaration>member;
|
||||
if (isStatic(property)) {
|
||||
const name = evaluator.nameOf(property.name);
|
||||
if (!isMetadataError(name)) {
|
||||
if (property.initializer) {
|
||||
const value = evaluator.evaluateNode(property.initializer);
|
||||
recordStaticMember(name, value);
|
||||
} else {
|
||||
recordStaticMember(name, errorSym('Variable not initialized', property.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
const propertyDecorators = getDecorators(property.decorators);
|
||||
if (propertyDecorators) {
|
||||
const name = evaluator.nameOf(property.name);
|
||||
if (!isMetadataError(name)) {
|
||||
recordMember(name, {__symbolic: 'property', decorators: propertyDecorators});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (members) {
|
||||
result.members = members;
|
||||
}
|
||||
if (statics) {
|
||||
result.statics = statics;
|
||||
}
|
||||
|
||||
return recordEntry(result, classDeclaration);
|
||||
}
|
||||
|
||||
// Collect all exported symbols from an exports clause.
|
||||
const exportMap = new Map<string, string>();
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
const exportDeclaration = <ts.ExportDeclaration>node;
|
||||
const {moduleSpecifier, exportClause} = exportDeclaration;
|
||||
|
||||
if (!moduleSpecifier) {
|
||||
exportClause.elements.forEach(spec => {
|
||||
const exportedAs = spec.name.text;
|
||||
const name = (spec.propertyName || spec.name).text;
|
||||
exportMap.set(name, exportedAs);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const isExportedIdentifier = (identifier: ts.Identifier) => exportMap.has(identifier.text);
|
||||
const isExported =
|
||||
(node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration |
|
||||
ts.EnumDeclaration) => isExport(node) || isExportedIdentifier(node.name);
|
||||
const exportedIdentifierName = (identifier: ts.Identifier) =>
|
||||
exportMap.get(identifier.text) || identifier.text;
|
||||
const exportedName =
|
||||
(node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration |
|
||||
ts.EnumDeclaration) => exportedIdentifierName(node.name);
|
||||
|
||||
|
||||
// Predeclare classes and functions
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
const classDeclaration = <ts.ClassDeclaration>node;
|
||||
if (classDeclaration.name) {
|
||||
const className = classDeclaration.name.text;
|
||||
if (isExported(classDeclaration)) {
|
||||
locals.define(
|
||||
className, {__symbolic: 'reference', name: exportedName(classDeclaration)});
|
||||
} else {
|
||||
locals.define(
|
||||
className, errorSym('Reference to non-exported class', node, {className}));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
const interfaceDeclaration = <ts.InterfaceDeclaration>node;
|
||||
if (interfaceDeclaration.name) {
|
||||
const interfaceName = interfaceDeclaration.name.text;
|
||||
// All references to interfaces should be converted to references to `any`.
|
||||
locals.define(interfaceName, {__symbolic: 'reference', name: 'any'});
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
const functionDeclaration = <ts.FunctionDeclaration>node;
|
||||
if (!isExported(functionDeclaration)) {
|
||||
// Report references to this function as an error.
|
||||
const nameNode = functionDeclaration.name;
|
||||
if (nameNode && nameNode.text) {
|
||||
locals.define(
|
||||
nameNode.text,
|
||||
errorSym(
|
||||
'Reference to a non-exported function', nameNode, {name: nameNode.text}));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
ts.forEachChild(sourceFile, node => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
// Record export declarations
|
||||
const exportDeclaration = <ts.ExportDeclaration>node;
|
||||
const {moduleSpecifier, exportClause} = exportDeclaration;
|
||||
|
||||
if (!moduleSpecifier) {
|
||||
// no module specifier -> export {propName as name};
|
||||
if (exportClause) {
|
||||
exportClause.elements.forEach(spec => {
|
||||
const name = spec.name.text;
|
||||
// If the symbol was not already exported, export a reference since it is a
|
||||
// reference to an import
|
||||
if (!metadata || !metadata[name]) {
|
||||
const propNode = spec.propertyName || spec.name;
|
||||
const value: MetadataValue = evaluator.evaluateNode(propNode);
|
||||
if (!metadata) metadata = {};
|
||||
metadata[name] = recordEntry(value, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleSpecifier && moduleSpecifier.kind == ts.SyntaxKind.StringLiteral) {
|
||||
// Ignore exports that don't have string literals as exports.
|
||||
// This is allowed by the syntax but will be flagged as an error by the type checker.
|
||||
const from = (<ts.StringLiteral>moduleSpecifier).text;
|
||||
const moduleExport: ModuleExportMetadata = {from};
|
||||
if (exportClause) {
|
||||
moduleExport.export = exportClause.elements.map(
|
||||
spec => spec.propertyName ? {name: spec.propertyName.text, as: spec.name.text} :
|
||||
spec.name.text);
|
||||
}
|
||||
if (!exports) exports = [];
|
||||
exports.push(moduleExport);
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
const classDeclaration = <ts.ClassDeclaration>node;
|
||||
if (classDeclaration.name) {
|
||||
if (isExported(classDeclaration)) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[exportedName(classDeclaration)] = classMetadataOf(classDeclaration);
|
||||
}
|
||||
}
|
||||
// Otherwise don't record metadata for the class.
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
const interfaceDeclaration = <ts.InterfaceDeclaration>node;
|
||||
if (interfaceDeclaration.name && isExported(interfaceDeclaration)) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[exportedName(interfaceDeclaration)] = {__symbolic: 'interface'};
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
// Record functions that return a single value. Record the parameter
|
||||
// names substitution will be performed by the StaticReflector.
|
||||
const functionDeclaration = <ts.FunctionDeclaration>node;
|
||||
if (isExported(functionDeclaration) && functionDeclaration.name) {
|
||||
if (!metadata) metadata = {};
|
||||
const name = exportedName(functionDeclaration);
|
||||
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
|
||||
metadata[name] =
|
||||
maybeFunc ? recordEntry(maybeFunc.func, node) : {__symbolic: 'function'};
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.EnumDeclaration:
|
||||
const enumDeclaration = <ts.EnumDeclaration>node;
|
||||
if (isExported(enumDeclaration)) {
|
||||
const enumValueHolder: {[name: string]: MetadataValue} = {};
|
||||
const enumName = exportedName(enumDeclaration);
|
||||
let nextDefaultValue: MetadataValue = 0;
|
||||
let writtenMembers = 0;
|
||||
for (const member of enumDeclaration.members) {
|
||||
let enumValue: MetadataValue;
|
||||
if (!member.initializer) {
|
||||
enumValue = nextDefaultValue;
|
||||
} else {
|
||||
enumValue = evaluator.evaluateNode(member.initializer);
|
||||
}
|
||||
let name: string = undefined;
|
||||
if (member.name.kind == ts.SyntaxKind.Identifier) {
|
||||
const identifier = <ts.Identifier>member.name;
|
||||
name = identifier.text;
|
||||
enumValueHolder[name] = enumValue;
|
||||
writtenMembers++;
|
||||
}
|
||||
if (typeof enumValue === 'number') {
|
||||
nextDefaultValue = enumValue + 1;
|
||||
} else if (name) {
|
||||
nextDefaultValue = {
|
||||
__symbolic: 'binary',
|
||||
operator: '+',
|
||||
left: {
|
||||
__symbolic: 'select',
|
||||
expression: recordEntry({__symbolic: 'reference', name: enumName}, node), name
|
||||
}
|
||||
};
|
||||
} else {
|
||||
nextDefaultValue =
|
||||
recordEntry(errorSym('Unsuppported enum member name', member.name), node);
|
||||
}
|
||||
}
|
||||
if (writtenMembers) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[enumName] = recordEntry(enumValueHolder, node);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
const variableStatement = <ts.VariableStatement>node;
|
||||
for (const variableDeclaration of variableStatement.declarationList.declarations) {
|
||||
if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) {
|
||||
const nameNode = <ts.Identifier>variableDeclaration.name;
|
||||
let varValue: MetadataValue;
|
||||
if (variableDeclaration.initializer) {
|
||||
varValue = evaluator.evaluateNode(variableDeclaration.initializer);
|
||||
} else {
|
||||
varValue = recordEntry(errorSym('Variable not initialized', nameNode), nameNode);
|
||||
}
|
||||
let exported = false;
|
||||
if (isExport(variableStatement) || isExport(variableDeclaration) ||
|
||||
isExportedIdentifier(nameNode)) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[exportedIdentifierName(nameNode)] = recordEntry(varValue, node);
|
||||
exported = true;
|
||||
}
|
||||
if (typeof varValue == 'string' || typeof varValue == 'number' ||
|
||||
typeof varValue == 'boolean') {
|
||||
locals.define(nameNode.text, varValue);
|
||||
if (exported) {
|
||||
locals.defineReference(
|
||||
nameNode.text, {__symbolic: 'reference', name: nameNode.text});
|
||||
}
|
||||
} else if (!exported) {
|
||||
if (varValue && !isMetadataError(varValue)) {
|
||||
locals.define(nameNode.text, recordEntry(varValue, node));
|
||||
} else {
|
||||
locals.define(
|
||||
nameNode.text,
|
||||
recordEntry(
|
||||
errorSym('Reference to a local symbol', nameNode, {name: nameNode.text}),
|
||||
node));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Destructuring (or binding) declarations are not supported,
|
||||
// var {<identifier>[, <identifier>]+} = <expression>;
|
||||
// or
|
||||
// var [<identifier>[, <identifier}+] = <expression>;
|
||||
// are not supported.
|
||||
const report: (nameNode: ts.Node) => void = (nameNode: ts.Node) => {
|
||||
switch (nameNode.kind) {
|
||||
case ts.SyntaxKind.Identifier:
|
||||
const name = <ts.Identifier>nameNode;
|
||||
const varValue = errorSym('Destructuring not supported', name);
|
||||
locals.define(name.text, varValue);
|
||||
if (isExport(node)) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[name.text] = varValue;
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.BindingElement:
|
||||
const bindingElement = <ts.BindingElement>nameNode;
|
||||
report(bindingElement.name);
|
||||
break;
|
||||
case ts.SyntaxKind.ObjectBindingPattern:
|
||||
case ts.SyntaxKind.ArrayBindingPattern:
|
||||
const bindings = <ts.BindingPattern>nameNode;
|
||||
(bindings as any).elements.forEach(report);
|
||||
break;
|
||||
}
|
||||
};
|
||||
report(variableDeclaration.name);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (metadata || exports) {
|
||||
if (!metadata)
|
||||
metadata = {};
|
||||
else if (strict) {
|
||||
validateMetadata(sourceFile, nodeMap, metadata);
|
||||
}
|
||||
const result: ModuleMetadata = {
|
||||
__symbolic: 'module',
|
||||
version: this.options.version || VERSION, metadata
|
||||
};
|
||||
if (exports) result.exports = exports;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This will throw if the metadata entry given contains an error node.
|
||||
function validateMetadata(
|
||||
sourceFile: ts.SourceFile, nodeMap: Map<MetadataEntry, ts.Node>,
|
||||
metadata: {[name: string]: MetadataEntry}) {
|
||||
let locals: Set<string> = new Set(['Array', 'Object', 'Set', 'Map', 'string', 'number', 'any']);
|
||||
|
||||
function validateExpression(
|
||||
expression: MetadataValue | MetadataSymbolicExpression | MetadataError) {
|
||||
if (!expression) {
|
||||
return;
|
||||
} else if (Array.isArray(expression)) {
|
||||
expression.forEach(validateExpression);
|
||||
} else if (typeof expression === 'object' && !expression.hasOwnProperty('__symbolic')) {
|
||||
Object.getOwnPropertyNames(expression).forEach(v => validateExpression((<any>expression)[v]));
|
||||
} else if (isMetadataError(expression)) {
|
||||
reportError(expression);
|
||||
} else if (isMetadataGlobalReferenceExpression(expression)) {
|
||||
if (!locals.has(expression.name)) {
|
||||
const reference = <MetadataValue>metadata[expression.name];
|
||||
if (reference) {
|
||||
validateExpression(reference);
|
||||
}
|
||||
}
|
||||
} else if (isFunctionMetadata(expression)) {
|
||||
validateFunction(<any>expression);
|
||||
} else if (isMetadataSymbolicExpression(expression)) {
|
||||
switch (expression.__symbolic) {
|
||||
case 'binary':
|
||||
const binaryExpression = <MetadataSymbolicBinaryExpression>expression;
|
||||
validateExpression(binaryExpression.left);
|
||||
validateExpression(binaryExpression.right);
|
||||
break;
|
||||
case 'call':
|
||||
case 'new':
|
||||
const callExpression = <MetadataSymbolicCallExpression>expression;
|
||||
validateExpression(callExpression.expression);
|
||||
if (callExpression.arguments) callExpression.arguments.forEach(validateExpression);
|
||||
break;
|
||||
case 'index':
|
||||
const indexExpression = <MetadataSymbolicIndexExpression>expression;
|
||||
validateExpression(indexExpression.expression);
|
||||
validateExpression(indexExpression.index);
|
||||
break;
|
||||
case 'pre':
|
||||
const prefixExpression = <MetadataSymbolicPrefixExpression>expression;
|
||||
validateExpression(prefixExpression.operand);
|
||||
break;
|
||||
case 'select':
|
||||
const selectExpression = <MetadataSymbolicSelectExpression>expression;
|
||||
validateExpression(selectExpression.expression);
|
||||
break;
|
||||
case 'spread':
|
||||
const spreadExpression = <MetadataSymbolicSpreadExpression>expression;
|
||||
validateExpression(spreadExpression.expression);
|
||||
break;
|
||||
case 'if':
|
||||
const ifExpression = <MetadataSymbolicIfExpression>expression;
|
||||
validateExpression(ifExpression.condition);
|
||||
validateExpression(ifExpression.elseExpression);
|
||||
validateExpression(ifExpression.thenExpression);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateMember(classData: ClassMetadata, member: MemberMetadata) {
|
||||
if (member.decorators) {
|
||||
member.decorators.forEach(validateExpression);
|
||||
}
|
||||
if (isMethodMetadata(member) && member.parameterDecorators) {
|
||||
member.parameterDecorators.forEach(validateExpression);
|
||||
}
|
||||
// Only validate parameters of classes for which we know that are used with our DI
|
||||
if (classData.decorators && isConstructorMetadata(member) && member.parameters) {
|
||||
member.parameters.forEach(validateExpression);
|
||||
}
|
||||
}
|
||||
|
||||
function validateClass(classData: ClassMetadata) {
|
||||
if (classData.decorators) {
|
||||
classData.decorators.forEach(validateExpression);
|
||||
}
|
||||
if (classData.members) {
|
||||
Object.getOwnPropertyNames(classData.members)
|
||||
.forEach(name => classData.members[name].forEach((m) => validateMember(classData, m)));
|
||||
}
|
||||
if (classData.statics) {
|
||||
Object.getOwnPropertyNames(classData.statics).forEach(name => {
|
||||
const staticMember = classData.statics[name];
|
||||
if (isFunctionMetadata(staticMember)) {
|
||||
validateExpression(staticMember.value);
|
||||
} else {
|
||||
validateExpression(staticMember);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateFunction(functionDeclaration: FunctionMetadata) {
|
||||
if (functionDeclaration.value) {
|
||||
const oldLocals = locals;
|
||||
if (functionDeclaration.parameters) {
|
||||
locals = new Set(oldLocals.values());
|
||||
if (functionDeclaration.parameters)
|
||||
functionDeclaration.parameters.forEach(n => locals.add(n));
|
||||
}
|
||||
validateExpression(functionDeclaration.value);
|
||||
locals = oldLocals;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldReportNode(node: ts.Node) {
|
||||
if (node) {
|
||||
const nodeStart = node.getStart();
|
||||
return !(
|
||||
node.pos != nodeStart &&
|
||||
sourceFile.text.substring(node.pos, nodeStart).indexOf('@dynamic') >= 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function reportError(error: MetadataError) {
|
||||
const node = nodeMap.get(error);
|
||||
if (shouldReportNode(node)) {
|
||||
const lineInfo = error.line != undefined ?
|
||||
error.character != undefined ? `:${error.line + 1}:${error.character + 1}` :
|
||||
`:${error.line + 1}` :
|
||||
'';
|
||||
throw new Error(
|
||||
`${sourceFile.fileName}${lineInfo}: Metadata collected contains an error that will be reported at runtime: ${expandedMessage(error)}.\n ${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
Object.getOwnPropertyNames(metadata).forEach(name => {
|
||||
const entry = metadata[name];
|
||||
try {
|
||||
if (isClassMetadata(entry)) {
|
||||
validateClass(entry);
|
||||
}
|
||||
} catch (e) {
|
||||
const node = nodeMap.get(entry);
|
||||
if (shouldReportNode(node)) {
|
||||
if (node) {
|
||||
const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
||||
throw new Error(
|
||||
`${sourceFile.fileName}:${line + 1}:${character + 1}: Error encountered in metadata generated for exported symbol '${name}': \n ${e.message}`);
|
||||
}
|
||||
throw new Error(
|
||||
`Error encountered in metadata generated for exported symbol ${name}: \n ${e.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collect parameter names from a function.
|
||||
function namesOf(parameters: ts.NodeArray<ts.ParameterDeclaration>): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
function addNamesOf(name: ts.Identifier | ts.BindingPattern) {
|
||||
if (name.kind == ts.SyntaxKind.Identifier) {
|
||||
const identifier = <ts.Identifier>name;
|
||||
result.push(identifier.text);
|
||||
} else {
|
||||
const bindingPattern = <ts.BindingPattern>name;
|
||||
for (const element of bindingPattern.elements) {
|
||||
const name = (element as any).name;
|
||||
if (name) {
|
||||
addNamesOf(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const parameter of parameters) {
|
||||
addNamesOf(parameter.name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function expandedMessage(error: any): string {
|
||||
switch (error.message) {
|
||||
case 'Reference to non-exported class':
|
||||
if (error.context && error.context.className) {
|
||||
return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`;
|
||||
}
|
||||
break;
|
||||
case 'Variable not initialized':
|
||||
return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler';
|
||||
case 'Destructuring not supported':
|
||||
return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring';
|
||||
case 'Could not resolve type':
|
||||
if (error.context && error.context.typeName) {
|
||||
return `Could not resolve type ${error.context.typeName}`;
|
||||
}
|
||||
break;
|
||||
case 'Function call not supported':
|
||||
let prefix =
|
||||
error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F';
|
||||
return prefix +
|
||||
'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function';
|
||||
case 'Reference to a local symbol':
|
||||
if (error.context && error.context.name) {
|
||||
return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`;
|
||||
}
|
||||
}
|
||||
return error.message;
|
||||
}
|
169
packages/tsc-wrapped/src/compiler_host.ts
Normal file
169
packages/tsc-wrapped/src/compiler_host.ts
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @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 {writeFileSync} from 'fs';
|
||||
import {normalize} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import NgOptions from './options';
|
||||
import {MetadataCollector} from './collector';
|
||||
import {ModuleMetadata} from './schema';
|
||||
|
||||
export function formatDiagnostics(d: ts.Diagnostic[]): string {
|
||||
const host: ts.FormatDiagnosticsHost = {
|
||||
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
|
||||
getNewLine: () => ts.sys.newLine,
|
||||
getCanonicalFileName: (f: string) => f
|
||||
};
|
||||
return ts.formatDiagnostics(d, host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of CompilerHost that forwards all methods to another instance.
|
||||
* Useful for partial implementations to override only methods they care about.
|
||||
*/
|
||||
export abstract class DelegatingHost implements ts.CompilerHost {
|
||||
constructor(protected delegate: ts.CompilerHost) {}
|
||||
getSourceFile =
|
||||
(fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) =>
|
||||
this.delegate.getSourceFile(fileName, languageVersion, onError);
|
||||
|
||||
getCancellationToken = () => this.delegate.getCancellationToken();
|
||||
getDefaultLibFileName = (options: ts.CompilerOptions) =>
|
||||
this.delegate.getDefaultLibFileName(options);
|
||||
getDefaultLibLocation = () => this.delegate.getDefaultLibLocation();
|
||||
writeFile: ts.WriteFileCallback = this.delegate.writeFile;
|
||||
getCurrentDirectory = () => this.delegate.getCurrentDirectory();
|
||||
getDirectories = (path: string): string[] =>
|
||||
(this.delegate as any).getDirectories?(this.delegate as any).getDirectories(path): [];
|
||||
getCanonicalFileName = (fileName: string) => this.delegate.getCanonicalFileName(fileName);
|
||||
useCaseSensitiveFileNames = () => this.delegate.useCaseSensitiveFileNames();
|
||||
getNewLine = () => this.delegate.getNewLine();
|
||||
fileExists = (fileName: string) => this.delegate.fileExists(fileName);
|
||||
readFile = (fileName: string) => this.delegate.readFile(fileName);
|
||||
trace = (s: string) => this.delegate.trace(s);
|
||||
directoryExists = (directoryName: string) => this.delegate.directoryExists(directoryName);
|
||||
}
|
||||
|
||||
const IGNORED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$/;
|
||||
const DTS = /\.d\.ts$/;
|
||||
|
||||
export class MetadataWriterHost extends DelegatingHost {
|
||||
private metadataCollector = new MetadataCollector({quotedNames: true});
|
||||
private metadataCollector1 = new MetadataCollector({version: 1});
|
||||
constructor(
|
||||
delegate: ts.CompilerHost, private ngOptions: NgOptions, private emitAllFiles: boolean) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
private writeMetadata(emitFilePath: string, sourceFile: ts.SourceFile) {
|
||||
// TODO: replace with DTS filePath when https://github.com/Microsoft/TypeScript/pull/8412 is
|
||||
// released
|
||||
if (/*DTS*/ /\.js$/.test(emitFilePath)) {
|
||||
const path = emitFilePath.replace(/*DTS*/ /\.js$/, '.metadata.json');
|
||||
|
||||
// Beginning with 2.1, TypeScript transforms the source tree before emitting it.
|
||||
// We need the original, unmodified, tree which might be several levels back
|
||||
// depending on the number of transforms performed. All SourceFile's prior to 2.1
|
||||
// will appear to be the original source since they didn't include an original field.
|
||||
let collectableFile = sourceFile;
|
||||
while ((collectableFile as any).original) {
|
||||
collectableFile = (collectableFile as any).original;
|
||||
}
|
||||
|
||||
const metadata =
|
||||
this.metadataCollector.getMetadata(collectableFile, !!this.ngOptions.strictMetadataEmit);
|
||||
const metadata1 = this.metadataCollector1.getMetadata(collectableFile, false);
|
||||
const metadatas: ModuleMetadata[] = [metadata, metadata1].filter(e => !!e);
|
||||
if (metadatas.length) {
|
||||
const metadataText = JSON.stringify(metadatas);
|
||||
writeFileSync(path, metadataText, {encoding: 'utf-8'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeFile: ts.WriteFileCallback =
|
||||
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
||||
const isDts = /\.d\.ts$/.test(fileName);
|
||||
if (this.emitAllFiles || isDts) {
|
||||
// Let the original file be written first; this takes care of creating parent directories
|
||||
this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||
}
|
||||
if (isDts) {
|
||||
// TODO: remove this early return after https://github.com/Microsoft/TypeScript/pull/8412
|
||||
// is released
|
||||
return;
|
||||
}
|
||||
|
||||
if (IGNORED_FILES.test(fileName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sourceFiles) {
|
||||
throw new Error(
|
||||
'Metadata emit requires the sourceFiles are passed to WriteFileCallback. ' +
|
||||
'Update to TypeScript ^1.9.0-dev');
|
||||
}
|
||||
if (sourceFiles.length > 1) {
|
||||
throw new Error('Bundled emit with --out is not supported');
|
||||
}
|
||||
if (!this.ngOptions.skipMetadataEmit && !this.ngOptions.flatModuleOutFile) {
|
||||
this.writeMetadata(fileName, sourceFiles[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SyntheticIndexHost extends DelegatingHost {
|
||||
private normalSyntheticIndexName: string;
|
||||
private indexContent: string;
|
||||
private indexMetadata: string;
|
||||
|
||||
constructor(
|
||||
delegate: ts.CompilerHost,
|
||||
syntheticIndex: {name: string, content: string, metadata: string}) {
|
||||
super(delegate);
|
||||
this.normalSyntheticIndexName = normalize(syntheticIndex.name);
|
||||
this.indexContent = syntheticIndex.content;
|
||||
this.indexMetadata = syntheticIndex.metadata;
|
||||
}
|
||||
|
||||
fileExists = (fileName: string):
|
||||
boolean => {
|
||||
return normalize(fileName) == this.normalSyntheticIndexName ||
|
||||
this.delegate.fileExists(fileName);
|
||||
}
|
||||
|
||||
readFile =
|
||||
(fileName: string) => {
|
||||
return normalize(fileName) == this.normalSyntheticIndexName ?
|
||||
this.indexContent :
|
||||
this.delegate.readFile(fileName);
|
||||
}
|
||||
|
||||
getSourceFile =
|
||||
(fileName: string, languageVersion: ts.ScriptTarget,
|
||||
onError?: (message: string) => void) => {
|
||||
if (normalize(fileName) == this.normalSyntheticIndexName) {
|
||||
return ts.createSourceFile(fileName, this.indexContent, 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 &&
|
||||
normalize(sourceFiles[0].fileName) == this.normalSyntheticIndexName) {
|
||||
// If we are writing the synthetic index, write the metadata along side.
|
||||
const metadataName = fileName.replace(DTS, '.metadata.json');
|
||||
writeFileSync(metadataName, this.indexMetadata, 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
668
packages/tsc-wrapped/src/evaluator.ts
Normal file
668
packages/tsc-wrapped/src/evaluator.ts
Normal file
@ -0,0 +1,668 @@
|
||||
/**
|
||||
* @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 ts from 'typescript';
|
||||
|
||||
import {CollectorOptions} from './collector';
|
||||
import {MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataValue, isMetadataError, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema';
|
||||
import {Symbols} from './symbols';
|
||||
|
||||
// In TypeScript 2.1 the spread element kind was renamed.
|
||||
const spreadElementSyntaxKind: ts.SyntaxKind =
|
||||
(ts.SyntaxKind as any).SpreadElement || (ts.SyntaxKind as any).SpreadElementExpression;
|
||||
|
||||
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
|
||||
const expression = callExpression.expression;
|
||||
if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
|
||||
const propertyAccessExpression = <ts.PropertyAccessExpression>expression;
|
||||
const name = propertyAccessExpression.name;
|
||||
if (name.kind == ts.SyntaxKind.Identifier) {
|
||||
return name.text === memberName;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isCallOf(callExpression: ts.CallExpression, ident: string): boolean {
|
||||
const expression = callExpression.expression;
|
||||
if (expression.kind === ts.SyntaxKind.Identifier) {
|
||||
const identifier = <ts.Identifier>expression;
|
||||
return identifier.text === ident;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* ts.forEachChild stops iterating children when the callback return a truthy value.
|
||||
* This method inverts this to implement an `every` style iterator. It will return
|
||||
* true if every call to `cb` returns `true`.
|
||||
*/
|
||||
function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) {
|
||||
return !ts.forEachChild(node, node => !cb(node));
|
||||
}
|
||||
|
||||
export function isPrimitive(value: any): boolean {
|
||||
return Object(value) !== value;
|
||||
}
|
||||
|
||||
function isDefined(obj: any): boolean {
|
||||
return obj !== undefined;
|
||||
}
|
||||
|
||||
// import {propertyName as name} from 'place'
|
||||
// import {name} from 'place'
|
||||
export interface ImportSpecifierMetadata {
|
||||
name: string;
|
||||
propertyName?: string;
|
||||
}
|
||||
export interface ImportMetadata {
|
||||
defaultName?: string; // import d from 'place'
|
||||
namespace?: string; // import * as d from 'place'
|
||||
namedImports?: ImportSpecifierMetadata[]; // import {a} from 'place'
|
||||
from: string; // from 'place'
|
||||
}
|
||||
|
||||
|
||||
function getSourceFileOfNode(node: ts.Node): ts.SourceFile {
|
||||
while (node && node.kind != ts.SyntaxKind.SourceFile) {
|
||||
node = node.parent;
|
||||
}
|
||||
return <ts.SourceFile>node;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function errorSymbol(
|
||||
message: string, node?: ts.Node, context?: {[name: string]: string},
|
||||
sourceFile?: ts.SourceFile): MetadataError {
|
||||
let result: MetadataError;
|
||||
if (node) {
|
||||
sourceFile = sourceFile || getSourceFileOfNode(node);
|
||||
if (sourceFile) {
|
||||
const {line, character} =
|
||||
ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
||||
result = {__symbolic: 'error', message, line, character};
|
||||
}
|
||||
}
|
||||
if (!result) {
|
||||
result = {__symbolic: 'error', message};
|
||||
}
|
||||
if (context) {
|
||||
result.context = context;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a symbolic representation of an expression folding values into their final value when
|
||||
* possible.
|
||||
*/
|
||||
export class Evaluator {
|
||||
constructor(
|
||||
private symbols: Symbols, private nodeMap: Map<MetadataEntry, ts.Node>,
|
||||
private options: CollectorOptions = {}) {}
|
||||
|
||||
nameOf(node: ts.Node): string|MetadataError {
|
||||
if (node.kind == ts.SyntaxKind.Identifier) {
|
||||
return (<ts.Identifier>node).text;
|
||||
}
|
||||
const result = this.evaluateNode(node);
|
||||
if (isMetadataError(result) || typeof result === 'string') {
|
||||
return result;
|
||||
} else {
|
||||
return errorSymbol('Name expected', node, {received: node.getText()});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the expression represented by `node` can be folded into a literal expression.
|
||||
*
|
||||
* For example, a literal is always foldable. This means that literal expressions such as `1.2`
|
||||
* `"Some value"` `true` `false` are foldable.
|
||||
*
|
||||
* - An object literal is foldable if all the properties in the literal are foldable.
|
||||
* - An array literal is foldable if all the elements are foldable.
|
||||
* - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR.
|
||||
* - A property access is foldable if the object is foldable.
|
||||
* - A array index is foldable if index expression is foldable and the array is foldable.
|
||||
* - Binary operator expressions are foldable if the left and right expressions are foldable and
|
||||
* it is one of '+', '-', '*', '/', '%', '||', and '&&'.
|
||||
* - An identifier is foldable if a value can be found for its symbol in the evaluator symbol
|
||||
* table.
|
||||
*/
|
||||
public isFoldable(node: ts.Node): boolean {
|
||||
return this.isFoldableWorker(node, new Map<ts.Node, boolean>());
|
||||
}
|
||||
|
||||
private isFoldableWorker(node: ts.Node, folding: Map<ts.Node, boolean>): boolean {
|
||||
if (node) {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||
return everyNodeChild(node, child => {
|
||||
if (child.kind === ts.SyntaxKind.PropertyAssignment) {
|
||||
const propertyAssignment = <ts.PropertyAssignment>child;
|
||||
return this.isFoldableWorker(propertyAssignment.initializer, folding);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
case ts.SyntaxKind.ArrayLiteralExpression:
|
||||
return everyNodeChild(node, child => this.isFoldableWorker(child, folding));
|
||||
case ts.SyntaxKind.CallExpression:
|
||||
const callExpression = <ts.CallExpression>node;
|
||||
// We can fold a <array>.concat(<v>).
|
||||
if (isMethodCallOf(callExpression, 'concat') &&
|
||||
arrayOrEmpty(callExpression.arguments).length === 1) {
|
||||
const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression;
|
||||
if (this.isFoldableWorker(arrayNode, folding) &&
|
||||
this.isFoldableWorker(callExpression.arguments[0], folding)) {
|
||||
// It needs to be an array.
|
||||
const arrayValue = this.evaluateNode(arrayNode);
|
||||
if (arrayValue && Array.isArray(arrayValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We can fold a call to CONST_EXPR
|
||||
if (isCallOf(callExpression, 'CONST_EXPR') &&
|
||||
arrayOrEmpty(callExpression.arguments).length === 1)
|
||||
return this.isFoldableWorker(callExpression.arguments[0], folding);
|
||||
return false;
|
||||
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
case ts.SyntaxKind.NumericLiteral:
|
||||
case ts.SyntaxKind.NullKeyword:
|
||||
case ts.SyntaxKind.TrueKeyword:
|
||||
case ts.SyntaxKind.FalseKeyword:
|
||||
case ts.SyntaxKind.TemplateHead:
|
||||
case ts.SyntaxKind.TemplateMiddle:
|
||||
case ts.SyntaxKind.TemplateTail:
|
||||
return true;
|
||||
case ts.SyntaxKind.ParenthesizedExpression:
|
||||
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
|
||||
return this.isFoldableWorker(parenthesizedExpression.expression, folding);
|
||||
case ts.SyntaxKind.BinaryExpression:
|
||||
const binaryExpression = <ts.BinaryExpression>node;
|
||||
switch (binaryExpression.operatorToken.kind) {
|
||||
case ts.SyntaxKind.PlusToken:
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
case ts.SyntaxKind.AsteriskToken:
|
||||
case ts.SyntaxKind.SlashToken:
|
||||
case ts.SyntaxKind.PercentToken:
|
||||
case ts.SyntaxKind.AmpersandAmpersandToken:
|
||||
case ts.SyntaxKind.BarBarToken:
|
||||
return this.isFoldableWorker(binaryExpression.left, folding) &&
|
||||
this.isFoldableWorker(binaryExpression.right, folding);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
case ts.SyntaxKind.PropertyAccessExpression:
|
||||
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
||||
return this.isFoldableWorker(propertyAccessExpression.expression, folding);
|
||||
case ts.SyntaxKind.ElementAccessExpression:
|
||||
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
||||
return this.isFoldableWorker(elementAccessExpression.expression, folding) &&
|
||||
this.isFoldableWorker(elementAccessExpression.argumentExpression, folding);
|
||||
case ts.SyntaxKind.Identifier:
|
||||
let identifier = <ts.Identifier>node;
|
||||
let reference = this.symbols.resolve(identifier.text);
|
||||
if (reference !== undefined && isPrimitive(reference)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.TemplateExpression:
|
||||
const templateExpression = <ts.TemplateExpression>node;
|
||||
return templateExpression.templateSpans.every(
|
||||
span => this.isFoldableWorker(span.expression, folding));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a JSON serialiable object representing `node`. The foldable values in the expression
|
||||
* tree are folded. For example, a node representing `1 + 2` is folded into `3`.
|
||||
*/
|
||||
public evaluateNode(node: ts.Node, preferReference?: boolean): MetadataValue {
|
||||
const t = this;
|
||||
let error: MetadataError|undefined;
|
||||
|
||||
function recordEntry<T extends MetadataEntry>(entry: T, node: ts.Node): T {
|
||||
t.nodeMap.set(entry, node);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function isFoldableError(value: any): value is MetadataError {
|
||||
return !t.options.verboseInvalidExpression && isMetadataError(value);
|
||||
}
|
||||
|
||||
const resolveName = (name: string, preferReference?: boolean): MetadataValue => {
|
||||
const reference = this.symbols.resolve(name, preferReference);
|
||||
if (reference === undefined) {
|
||||
// Encode as a global reference. StaticReflector will check the reference.
|
||||
return recordEntry({__symbolic: 'reference', name}, node);
|
||||
}
|
||||
return reference;
|
||||
};
|
||||
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ObjectLiteralExpression:
|
||||
let obj: {[name: string]: any} = {};
|
||||
let quoted: string[] = [];
|
||||
ts.forEachChild(node, child => {
|
||||
switch (child.kind) {
|
||||
case ts.SyntaxKind.ShorthandPropertyAssignment:
|
||||
case ts.SyntaxKind.PropertyAssignment:
|
||||
const assignment = <ts.PropertyAssignment|ts.ShorthandPropertyAssignment>child;
|
||||
if (assignment.name.kind == ts.SyntaxKind.StringLiteral) {
|
||||
const name = (assignment.name as ts.StringLiteral).text;
|
||||
quoted.push(name);
|
||||
}
|
||||
const propertyName = this.nameOf(assignment.name);
|
||||
if (isFoldableError(propertyName)) {
|
||||
error = propertyName;
|
||||
return true;
|
||||
}
|
||||
const propertyValue = isPropertyAssignment(assignment) ?
|
||||
this.evaluateNode(assignment.initializer, /* preferReference */ true) :
|
||||
resolveName(propertyName, /* preferReference */ true);
|
||||
if (isFoldableError(propertyValue)) {
|
||||
error = propertyValue;
|
||||
return true; // Stop the forEachChild.
|
||||
} else {
|
||||
obj[<string>propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (error) return error;
|
||||
if (this.options.quotedNames && quoted.length) {
|
||||
obj['$quoted$'] = quoted;
|
||||
}
|
||||
return obj;
|
||||
case ts.SyntaxKind.ArrayLiteralExpression:
|
||||
let arr: MetadataValue[] = [];
|
||||
ts.forEachChild(node, child => {
|
||||
const value = this.evaluateNode(child, /* preferReference */ true);
|
||||
|
||||
// Check for error
|
||||
if (isFoldableError(value)) {
|
||||
error = value;
|
||||
return true; // Stop the forEachChild.
|
||||
}
|
||||
|
||||
// Handle spread expressions
|
||||
if (isMetadataSymbolicSpreadExpression(value)) {
|
||||
if (Array.isArray(value.expression)) {
|
||||
for (const spreadValue of value.expression) {
|
||||
arr.push(spreadValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
arr.push(value);
|
||||
});
|
||||
if (error) return error;
|
||||
return arr;
|
||||
case spreadElementSyntaxKind:
|
||||
let spreadExpression = this.evaluateNode((node as any).expression);
|
||||
return recordEntry({__symbolic: 'spread', expression: spreadExpression}, node);
|
||||
case ts.SyntaxKind.CallExpression:
|
||||
const callExpression = <ts.CallExpression>node;
|
||||
if (isCallOf(callExpression, 'forwardRef') &&
|
||||
arrayOrEmpty(callExpression.arguments).length === 1) {
|
||||
const firstArgument = callExpression.arguments[0];
|
||||
if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) {
|
||||
const arrowFunction = <ts.ArrowFunction>firstArgument;
|
||||
return recordEntry(this.evaluateNode(arrowFunction.body), node);
|
||||
}
|
||||
}
|
||||
const args = arrayOrEmpty(callExpression.arguments).map(arg => this.evaluateNode(arg));
|
||||
if (!this.options.verboseInvalidExpression && args.some(isMetadataError)) {
|
||||
return args.find(isMetadataError);
|
||||
}
|
||||
if (this.isFoldable(callExpression)) {
|
||||
if (isMethodCallOf(callExpression, 'concat')) {
|
||||
const arrayValue = <MetadataValue[]>this.evaluateNode(
|
||||
(<ts.PropertyAccessExpression>callExpression.expression).expression);
|
||||
if (isFoldableError(arrayValue)) return arrayValue;
|
||||
return arrayValue.concat(args[0]);
|
||||
}
|
||||
}
|
||||
// Always fold a CONST_EXPR even if the argument is not foldable.
|
||||
if (isCallOf(callExpression, 'CONST_EXPR') &&
|
||||
arrayOrEmpty(callExpression.arguments).length === 1) {
|
||||
return recordEntry(args[0], node);
|
||||
}
|
||||
const expression = this.evaluateNode(callExpression.expression);
|
||||
if (isFoldableError(expression)) {
|
||||
return recordEntry(expression, node);
|
||||
}
|
||||
let result: MetadataSymbolicCallExpression = {__symbolic: 'call', expression: expression};
|
||||
if (args && args.length) {
|
||||
result.arguments = args;
|
||||
}
|
||||
return recordEntry(result, node);
|
||||
case ts.SyntaxKind.NewExpression:
|
||||
const newExpression = <ts.NewExpression>node;
|
||||
const newArgs = arrayOrEmpty(newExpression.arguments).map(arg => this.evaluateNode(arg));
|
||||
if (!this.options.verboseInvalidExpression && newArgs.some(isMetadataError)) {
|
||||
return recordEntry(newArgs.find(isMetadataError), node);
|
||||
}
|
||||
const newTarget = this.evaluateNode(newExpression.expression);
|
||||
if (isMetadataError(newTarget)) {
|
||||
return recordEntry(newTarget, node);
|
||||
}
|
||||
const call: MetadataSymbolicCallExpression = {__symbolic: 'new', expression: newTarget};
|
||||
if (newArgs.length) {
|
||||
call.arguments = newArgs;
|
||||
}
|
||||
return recordEntry(call, node);
|
||||
case ts.SyntaxKind.PropertyAccessExpression: {
|
||||
const propertyAccessExpression = <ts.PropertyAccessExpression>node;
|
||||
const expression = this.evaluateNode(propertyAccessExpression.expression);
|
||||
if (isFoldableError(expression)) {
|
||||
return recordEntry(expression, node);
|
||||
}
|
||||
const member = this.nameOf(propertyAccessExpression.name);
|
||||
if (isFoldableError(member)) {
|
||||
return recordEntry(member, node);
|
||||
}
|
||||
if (expression && this.isFoldable(propertyAccessExpression.expression))
|
||||
return (<any>expression)[<string>member];
|
||||
if (isMetadataModuleReferenceExpression(expression)) {
|
||||
// A select into a module reference and be converted into a reference to the symbol
|
||||
// in the module
|
||||
return recordEntry(
|
||||
{__symbolic: 'reference', module: expression.module, name: member}, node);
|
||||
}
|
||||
return recordEntry({__symbolic: 'select', expression, member}, node);
|
||||
}
|
||||
case ts.SyntaxKind.ElementAccessExpression: {
|
||||
const elementAccessExpression = <ts.ElementAccessExpression>node;
|
||||
const expression = this.evaluateNode(elementAccessExpression.expression);
|
||||
if (isFoldableError(expression)) {
|
||||
return recordEntry(expression, node);
|
||||
}
|
||||
if (!elementAccessExpression.argumentExpression) {
|
||||
return recordEntry(errorSymbol('Expression form not supported', node), node);
|
||||
}
|
||||
const index = this.evaluateNode(elementAccessExpression.argumentExpression);
|
||||
if (isFoldableError(expression)) {
|
||||
return recordEntry(expression, node);
|
||||
}
|
||||
if (this.isFoldable(elementAccessExpression.expression) &&
|
||||
this.isFoldable(elementAccessExpression.argumentExpression))
|
||||
return (<any>expression)[<string|number>index];
|
||||
return recordEntry({__symbolic: 'index', expression, index}, node);
|
||||
}
|
||||
case ts.SyntaxKind.Identifier:
|
||||
const identifier = <ts.Identifier>node;
|
||||
const name = identifier.text;
|
||||
return resolveName(name, preferReference);
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
const typeReferenceNode = <ts.TypeReferenceNode>node;
|
||||
const typeNameNode = typeReferenceNode.typeName;
|
||||
const getReference: (typeNameNode: ts.Identifier | ts.QualifiedName) => MetadataValue =
|
||||
node => {
|
||||
if (typeNameNode.kind === ts.SyntaxKind.QualifiedName) {
|
||||
const qualifiedName = <ts.QualifiedName>node;
|
||||
const left = this.evaluateNode(qualifiedName.left);
|
||||
if (isMetadataModuleReferenceExpression(left)) {
|
||||
return recordEntry(
|
||||
<MetadataImportedSymbolReferenceExpression>{
|
||||
__symbolic: 'reference',
|
||||
module: left.module,
|
||||
name: qualifiedName.right.text
|
||||
},
|
||||
node);
|
||||
}
|
||||
// Record a type reference to a declared type as a select.
|
||||
return {__symbolic: 'select', expression: left, member: qualifiedName.right.text};
|
||||
} else {
|
||||
const identifier = <ts.Identifier>typeNameNode;
|
||||
const symbol = this.symbols.resolve(identifier.text);
|
||||
if (isFoldableError(symbol) || isMetadataSymbolicReferenceExpression(symbol)) {
|
||||
return recordEntry(symbol, node);
|
||||
}
|
||||
return recordEntry(
|
||||
errorSymbol('Could not resolve type', node, {typeName: identifier.text}), node);
|
||||
}
|
||||
};
|
||||
const typeReference = getReference(typeNameNode);
|
||||
if (isFoldableError(typeReference)) {
|
||||
return recordEntry(typeReference, node);
|
||||
}
|
||||
if (!isMetadataModuleReferenceExpression(typeReference) &&
|
||||
typeReferenceNode.typeArguments && typeReferenceNode.typeArguments.length) {
|
||||
const args = typeReferenceNode.typeArguments.map(element => this.evaluateNode(element));
|
||||
// TODO: Remove typecast when upgraded to 2.0 as it will be corretly inferred.
|
||||
// Some versions of 1.9 do not infer this correctly.
|
||||
(<MetadataImportedSymbolReferenceExpression>typeReference).arguments = args;
|
||||
}
|
||||
return recordEntry(typeReference, node);
|
||||
case ts.SyntaxKind.UnionType:
|
||||
const unionType = <ts.UnionTypeNode>node;
|
||||
|
||||
// Remove null and undefined from the list of unions.
|
||||
const references = unionType.types
|
||||
.filter(
|
||||
n => n.kind != ts.SyntaxKind.NullKeyword &&
|
||||
n.kind != ts.SyntaxKind.UndefinedKeyword)
|
||||
.map(n => this.evaluateNode(n));
|
||||
|
||||
// The remmaining reference must be the same. If two have type arguments consider them
|
||||
// different even if the type arguments are the same.
|
||||
let candidate: any = null;
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
const reference = references[i];
|
||||
if (isMetadataSymbolicReferenceExpression(reference)) {
|
||||
if (candidate) {
|
||||
if ((reference as any).name == candidate.name &&
|
||||
(reference as any).module == candidate.module && !(reference as any).arguments) {
|
||||
candidate = reference;
|
||||
}
|
||||
} else {
|
||||
candidate = reference;
|
||||
}
|
||||
} else {
|
||||
return reference;
|
||||
}
|
||||
}
|
||||
if (candidate) return candidate;
|
||||
break;
|
||||
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
||||
case ts.SyntaxKind.StringLiteral:
|
||||
case ts.SyntaxKind.TemplateHead:
|
||||
case ts.SyntaxKind.TemplateTail:
|
||||
case ts.SyntaxKind.TemplateMiddle:
|
||||
return (<ts.LiteralLikeNode>node).text;
|
||||
case ts.SyntaxKind.NumericLiteral:
|
||||
return parseFloat((<ts.LiteralExpression>node).text);
|
||||
case ts.SyntaxKind.AnyKeyword:
|
||||
return recordEntry({__symbolic: 'reference', name: 'any'}, node);
|
||||
case ts.SyntaxKind.StringKeyword:
|
||||
return recordEntry({__symbolic: 'reference', name: 'string'}, node);
|
||||
case ts.SyntaxKind.NumberKeyword:
|
||||
return recordEntry({__symbolic: 'reference', name: 'number'}, node);
|
||||
case ts.SyntaxKind.BooleanKeyword:
|
||||
return recordEntry({__symbolic: 'reference', name: 'boolean'}, node);
|
||||
case ts.SyntaxKind.ArrayType:
|
||||
const arrayTypeNode = <ts.ArrayTypeNode>node;
|
||||
return recordEntry(
|
||||
{
|
||||
__symbolic: 'reference',
|
||||
name: 'Array',
|
||||
arguments: [this.evaluateNode(arrayTypeNode.elementType)]
|
||||
},
|
||||
node);
|
||||
case ts.SyntaxKind.NullKeyword:
|
||||
return null;
|
||||
case ts.SyntaxKind.TrueKeyword:
|
||||
return true;
|
||||
case ts.SyntaxKind.FalseKeyword:
|
||||
return false;
|
||||
case ts.SyntaxKind.ParenthesizedExpression:
|
||||
const parenthesizedExpression = <ts.ParenthesizedExpression>node;
|
||||
return this.evaluateNode(parenthesizedExpression.expression);
|
||||
case ts.SyntaxKind.TypeAssertionExpression:
|
||||
const typeAssertion = <ts.TypeAssertion>node;
|
||||
return this.evaluateNode(typeAssertion.expression);
|
||||
case ts.SyntaxKind.PrefixUnaryExpression:
|
||||
const prefixUnaryExpression = <ts.PrefixUnaryExpression>node;
|
||||
const operand = this.evaluateNode(prefixUnaryExpression.operand);
|
||||
if (isDefined(operand) && isPrimitive(operand)) {
|
||||
switch (prefixUnaryExpression.operator) {
|
||||
case ts.SyntaxKind.PlusToken:
|
||||
return +operand;
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
return -operand;
|
||||
case ts.SyntaxKind.TildeToken:
|
||||
return ~operand;
|
||||
case ts.SyntaxKind.ExclamationToken:
|
||||
return !operand;
|
||||
}
|
||||
}
|
||||
let operatorText: string;
|
||||
switch (prefixUnaryExpression.operator) {
|
||||
case ts.SyntaxKind.PlusToken:
|
||||
operatorText = '+';
|
||||
break;
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
operatorText = '-';
|
||||
break;
|
||||
case ts.SyntaxKind.TildeToken:
|
||||
operatorText = '~';
|
||||
break;
|
||||
case ts.SyntaxKind.ExclamationToken:
|
||||
operatorText = '!';
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
return recordEntry({__symbolic: 'pre', operator: operatorText, operand: operand}, node);
|
||||
case ts.SyntaxKind.BinaryExpression:
|
||||
const binaryExpression = <ts.BinaryExpression>node;
|
||||
const left = this.evaluateNode(binaryExpression.left);
|
||||
const right = this.evaluateNode(binaryExpression.right);
|
||||
if (isDefined(left) && isDefined(right)) {
|
||||
if (isPrimitive(left) && isPrimitive(right))
|
||||
switch (binaryExpression.operatorToken.kind) {
|
||||
case ts.SyntaxKind.BarBarToken:
|
||||
return <any>left || <any>right;
|
||||
case ts.SyntaxKind.AmpersandAmpersandToken:
|
||||
return <any>left && <any>right;
|
||||
case ts.SyntaxKind.AmpersandToken:
|
||||
return <any>left & <any>right;
|
||||
case ts.SyntaxKind.BarToken:
|
||||
return <any>left | <any>right;
|
||||
case ts.SyntaxKind.CaretToken:
|
||||
return <any>left ^ <any>right;
|
||||
case ts.SyntaxKind.EqualsEqualsToken:
|
||||
return <any>left == <any>right;
|
||||
case ts.SyntaxKind.ExclamationEqualsToken:
|
||||
return <any>left != <any>right;
|
||||
case ts.SyntaxKind.EqualsEqualsEqualsToken:
|
||||
return <any>left === <any>right;
|
||||
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
|
||||
return <any>left !== <any>right;
|
||||
case ts.SyntaxKind.LessThanToken:
|
||||
return <any>left < <any>right;
|
||||
case ts.SyntaxKind.GreaterThanToken:
|
||||
return <any>left > <any>right;
|
||||
case ts.SyntaxKind.LessThanEqualsToken:
|
||||
return <any>left <= <any>right;
|
||||
case ts.SyntaxKind.GreaterThanEqualsToken:
|
||||
return <any>left >= <any>right;
|
||||
case ts.SyntaxKind.LessThanLessThanToken:
|
||||
return (<any>left) << (<any>right);
|
||||
case ts.SyntaxKind.GreaterThanGreaterThanToken:
|
||||
return <any>left >> <any>right;
|
||||
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
|
||||
return <any>left >>> <any>right;
|
||||
case ts.SyntaxKind.PlusToken:
|
||||
return <any>left + <any>right;
|
||||
case ts.SyntaxKind.MinusToken:
|
||||
return <any>left - <any>right;
|
||||
case ts.SyntaxKind.AsteriskToken:
|
||||
return <any>left * <any>right;
|
||||
case ts.SyntaxKind.SlashToken:
|
||||
return <any>left / <any>right;
|
||||
case ts.SyntaxKind.PercentToken:
|
||||
return <any>left % <any>right;
|
||||
}
|
||||
return recordEntry(
|
||||
{
|
||||
__symbolic: 'binop',
|
||||
operator: binaryExpression.operatorToken.getText(),
|
||||
left: left,
|
||||
right: right
|
||||
},
|
||||
node);
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.ConditionalExpression:
|
||||
const conditionalExpression = <ts.ConditionalExpression>node;
|
||||
const condition = this.evaluateNode(conditionalExpression.condition);
|
||||
const thenExpression = this.evaluateNode(conditionalExpression.whenTrue);
|
||||
const elseExpression = this.evaluateNode(conditionalExpression.whenFalse);
|
||||
if (isPrimitive(condition)) {
|
||||
return condition ? thenExpression : elseExpression;
|
||||
}
|
||||
return recordEntry({__symbolic: 'if', condition, thenExpression, elseExpression}, node);
|
||||
case ts.SyntaxKind.FunctionExpression:
|
||||
case ts.SyntaxKind.ArrowFunction:
|
||||
return recordEntry(errorSymbol('Function call not supported', node), node);
|
||||
case ts.SyntaxKind.TaggedTemplateExpression:
|
||||
return recordEntry(
|
||||
errorSymbol('Tagged template expressions are not supported in metadata', node), node);
|
||||
case ts.SyntaxKind.TemplateExpression:
|
||||
const templateExpression = <ts.TemplateExpression>node;
|
||||
if (this.isFoldable(node)) {
|
||||
return templateExpression.templateSpans.reduce(
|
||||
(previous, current) => previous + <string>this.evaluateNode(current.expression) +
|
||||
<string>this.evaluateNode(current.literal),
|
||||
this.evaluateNode(templateExpression.head));
|
||||
} else {
|
||||
return templateExpression.templateSpans.reduce((previous, current) => {
|
||||
const expr = this.evaluateNode(current.expression);
|
||||
const literal = this.evaluateNode(current.literal);
|
||||
if (isFoldableError(expr)) return expr;
|
||||
if (isFoldableError(literal)) return literal;
|
||||
if (typeof previous === 'string' && typeof expr === 'string' &&
|
||||
typeof literal === 'string') {
|
||||
return previous + expr + literal;
|
||||
}
|
||||
let result = expr;
|
||||
if (previous !== '') {
|
||||
result = {__symbolic: 'binop', operator: '+', left: previous, right: expr};
|
||||
}
|
||||
if (literal != '') {
|
||||
result = {__symbolic: 'binop', operator: '+', left: result, right: literal};
|
||||
}
|
||||
return result;
|
||||
}, this.evaluateNode(templateExpression.head));
|
||||
}
|
||||
case ts.SyntaxKind.AsExpression:
|
||||
const asExpression = <ts.AsExpression>node;
|
||||
return this.evaluateNode(asExpression.expression);
|
||||
case ts.SyntaxKind.ClassExpression:
|
||||
return {__symbolic: 'class'};
|
||||
}
|
||||
return recordEntry(errorSymbol('Expression form not supported', node), node);
|
||||
}
|
||||
}
|
||||
|
||||
function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment {
|
||||
return node.kind == ts.SyntaxKind.PropertyAssignment;
|
||||
}
|
||||
|
||||
const empty = [] as ts.NodeArray<any>;
|
||||
|
||||
function arrayOrEmpty<T extends ts.Node>(v: ts.NodeArray<T>): ts.NodeArray<T> {
|
||||
return v || empty;
|
||||
}
|
58
packages/tsc-wrapped/src/index_writer.ts
Normal file
58
packages/tsc-wrapped/src/index_writer.ts
Normal file
@ -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;
|
||||
};
|
||||
}
|
211
packages/tsc-wrapped/src/main.ts
Normal file
211
packages/tsc-wrapped/src/main.ts
Normal file
@ -0,0 +1,211 @@
|
||||
/**
|
||||
* @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 tsickle from 'tsickle';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CompilerHostAdapter, MetadataBundler} from './bundler';
|
||||
import {CliOptions} from './cli_options';
|
||||
import {MetadataWriterHost, SyntheticIndexHost} from './compiler_host';
|
||||
import {privateEntriesToIndex} from './index_writer';
|
||||
import NgOptions from './options';
|
||||
import {check, tsc} from './tsc';
|
||||
import {isVinylFile, VinylFile} from './vinyl_file';
|
||||
|
||||
export {UserError} from './tsc';
|
||||
|
||||
const DTS = /\.d\.ts$/;
|
||||
const JS_EXT = /(\.js|)$/;
|
||||
const TS_EXT = /\.ts$/;
|
||||
|
||||
export interface CodegenExtension {
|
||||
/**
|
||||
* Returns the generated file names.
|
||||
*/
|
||||
(ngOptions: NgOptions, cliOptions: CliOptions, program: ts.Program,
|
||||
host: ts.CompilerHost): Promise<string[]>;
|
||||
}
|
||||
|
||||
export function createBundleIndexHost(
|
||||
ngOptions: NgOptions, rootFiles: string[],
|
||||
host: ts.CompilerHost): {host: ts.CompilerHost, indexName?: string, errors?: ts.Diagnostic[]} {
|
||||
const files = rootFiles.filter(f => !DTS.test(f));
|
||||
if (files.length != 1) {
|
||||
return {
|
||||
host,
|
||||
errors: [{
|
||||
file: null,
|
||||
start: null,
|
||||
length: null,
|
||||
messageText:
|
||||
'Angular compiler option "flatModuleIndex" requires one and only one .ts file in the "files" field.',
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0
|
||||
}]
|
||||
};
|
||||
}
|
||||
const file = files[0];
|
||||
const indexModule = file.replace(/\.ts$/, '');
|
||||
const bundler =
|
||||
new MetadataBundler(indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host));
|
||||
const metadataBundle = bundler.getMetadataBundle();
|
||||
const metadata = JSON.stringify(metadataBundle.metadata);
|
||||
const name =
|
||||
path.join(path.dirname(indexModule), ngOptions.flatModuleOutFile.replace(JS_EXT, '.ts'));
|
||||
const libraryIndex = `./${path.basename(indexModule)}`;
|
||||
const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates);
|
||||
host = new SyntheticIndexHost(host, {name, content, metadata});
|
||||
return {host, indexName: name};
|
||||
}
|
||||
|
||||
export function main(
|
||||
project: string | VinylFile, cliOptions: CliOptions, codegen?: CodegenExtension,
|
||||
options?: ts.CompilerOptions): Promise<any> {
|
||||
try {
|
||||
let projectDir = project;
|
||||
// project is vinyl like file object
|
||||
if (isVinylFile(project)) {
|
||||
projectDir = path.dirname(project.path);
|
||||
}
|
||||
// project is path to project file
|
||||
else if (fs.lstatSync(project).isFile()) {
|
||||
projectDir = path.dirname(project);
|
||||
}
|
||||
|
||||
// file names in tsconfig are resolved relative to this absolute path
|
||||
const basePath = path.resolve(process.cwd(), cliOptions.basePath || projectDir);
|
||||
|
||||
// read the configuration options from wherever you store them
|
||||
let {parsed, ngOptions} = tsc.readConfiguration(project, basePath, options);
|
||||
ngOptions.basePath = basePath;
|
||||
let rootFileNames: string[] = parsed.fileNames.slice(0);
|
||||
const createProgram = (host: ts.CompilerHost, oldProgram?: ts.Program) => {
|
||||
return ts.createProgram(rootFileNames.slice(0), parsed.options, host, oldProgram);
|
||||
};
|
||||
const addGeneratedFileName = (genFileName: string) => {
|
||||
if (genFileName.startsWith(basePath) && TS_EXT.exec(genFileName)) {
|
||||
rootFileNames.push(genFileName);
|
||||
}
|
||||
};
|
||||
|
||||
const diagnostics = (parsed.options as any).diagnostics;
|
||||
if (diagnostics) (ts as any).performance.enable();
|
||||
|
||||
let host = ts.createCompilerHost(parsed.options, true);
|
||||
|
||||
// If the compilation is a flat module index then produce the flat module index
|
||||
// metadata and the synthetic flat module index.
|
||||
if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) {
|
||||
const {host: bundleHost, indexName, errors} =
|
||||
createBundleIndexHost(ngOptions, rootFileNames, host);
|
||||
if (errors) check(errors);
|
||||
if (indexName) addGeneratedFileName(indexName);
|
||||
host = bundleHost;
|
||||
}
|
||||
|
||||
const tsickleCompilerHostOptions:
|
||||
tsickle.Options = {googmodule: false, untyped: true, convertIndexImportShorthand: true};
|
||||
|
||||
const tsickleHost: tsickle.TsickleHost = {
|
||||
shouldSkipTsickleProcessing: (fileName) => /\.d\.ts$/.test(fileName),
|
||||
pathToModuleName: (context, importPath) => '',
|
||||
shouldIgnoreWarningsForPath: (filePath) => false,
|
||||
fileNameToModuleId: (fileName) => fileName,
|
||||
};
|
||||
|
||||
const tsickleCompilerHost =
|
||||
new tsickle.TsickleCompilerHost(host, ngOptions, tsickleCompilerHostOptions, tsickleHost);
|
||||
|
||||
const program = createProgram(tsickleCompilerHost);
|
||||
|
||||
const errors = program.getOptionsDiagnostics();
|
||||
check(errors);
|
||||
|
||||
if (ngOptions.skipTemplateCodegen || !codegen) {
|
||||
codegen = () => Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (diagnostics) console.time('NG codegen');
|
||||
return codegen(ngOptions, cliOptions, program, host).then((genFiles) => {
|
||||
if (diagnostics) console.timeEnd('NG codegen');
|
||||
|
||||
// Add the generated files to the configuration so they will become part of the program.
|
||||
if (ngOptions.alwaysCompileGeneratedCode) {
|
||||
genFiles.forEach(genFileName => addGeneratedFileName(genFileName));
|
||||
}
|
||||
let definitionsHost: ts.CompilerHost = tsickleCompilerHost;
|
||||
if (!ngOptions.skipMetadataEmit) {
|
||||
// if tsickle is not not used for emitting, but we do use the MetadataWriterHost,
|
||||
// it also needs to emit the js files.
|
||||
const emitJsFiles =
|
||||
ngOptions.annotationsAs === 'decorators' && !ngOptions.annotateForClosureCompiler;
|
||||
definitionsHost = new MetadataWriterHost(tsickleCompilerHost, ngOptions, emitJsFiles);
|
||||
}
|
||||
|
||||
// Create a new program since codegen files were created after making the old program
|
||||
let programWithCodegen = createProgram(definitionsHost, program);
|
||||
tsc.typeCheck(host, programWithCodegen);
|
||||
|
||||
let programForJsEmit = programWithCodegen;
|
||||
|
||||
if (ngOptions.annotationsAs !== 'decorators') {
|
||||
if (diagnostics) console.time('NG downlevel');
|
||||
tsickleCompilerHost.reconfigureForRun(programForJsEmit, tsickle.Pass.DECORATOR_DOWNLEVEL);
|
||||
// A program can be re-used only once; save the programWithCodegen to be reused by
|
||||
// metadataWriter
|
||||
programForJsEmit = createProgram(tsickleCompilerHost);
|
||||
check(tsickleCompilerHost.diagnostics);
|
||||
if (diagnostics) console.timeEnd('NG downlevel');
|
||||
}
|
||||
|
||||
if (ngOptions.annotateForClosureCompiler) {
|
||||
if (diagnostics) console.time('NG JSDoc');
|
||||
tsickleCompilerHost.reconfigureForRun(programForJsEmit, tsickle.Pass.CLOSURIZE);
|
||||
programForJsEmit = createProgram(tsickleCompilerHost);
|
||||
check(tsickleCompilerHost.diagnostics);
|
||||
if (diagnostics) console.timeEnd('NG JSDoc');
|
||||
}
|
||||
|
||||
// Emit *.js and *.js.map
|
||||
tsc.emit(programForJsEmit);
|
||||
|
||||
// Emit *.d.ts and maybe *.metadata.json
|
||||
// Not in the same emit pass with above, because tsickle erases
|
||||
// decorators which we want to read or document.
|
||||
// Do this emit second since TypeScript will create missing directories for us
|
||||
// in the standard emit.
|
||||
tsc.emit(programWithCodegen);
|
||||
|
||||
if (diagnostics) {
|
||||
(ts as any).performance.forEachMeasure(
|
||||
(name: string, duration: number) => { console.error(`TS ${name}: ${duration}ms`); });
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
let {options, errors} = (ts as any).parseCommandLine(args);
|
||||
check(errors);
|
||||
const project = options.project || '.';
|
||||
// TODO(alexeagle): command line should be TSC-compatible, remove "CliOptions" here
|
||||
const cliOptions = new CliOptions(require('minimist')(args));
|
||||
main(project, cliOptions, null, options)
|
||||
.then((exitCode: any) => process.exit(exitCode))
|
||||
.catch((e: any) => {
|
||||
console.error(e.stack);
|
||||
console.error('Compilation failed');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
95
packages/tsc-wrapped/src/options.ts
Normal file
95
packages/tsc-wrapped/src/options.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @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 ts from 'typescript';
|
||||
|
||||
interface Options extends ts.CompilerOptions {
|
||||
// Absolute path to a directory where generated file structure is written.
|
||||
// If unspecified, generated files will be written alongside sources.
|
||||
genDir?: string;
|
||||
|
||||
// Path to the directory containing the tsconfig.json file.
|
||||
basePath?: string;
|
||||
|
||||
// Don't produce .metadata.json files (they don't work for bundled emit with --out)
|
||||
skipMetadataEmit?: boolean;
|
||||
|
||||
// Produce an error if the metadata written for a class would produce an error if used.
|
||||
strictMetadataEmit?: boolean;
|
||||
|
||||
// Don't produce .ngfactory.ts or .ngstyle.ts files
|
||||
skipTemplateCodegen?: boolean;
|
||||
|
||||
// Whether to generate a flat module index of the given name and the corresponding
|
||||
// flat module metadata. This option is intended to be used when creating flat
|
||||
// modules similar to how `@angular/core` and `@angular/common` are packaged.
|
||||
// When this option is used the `package.json` for the library should referred to the
|
||||
// generated flat module index instead of the library index file. When using this
|
||||
// option only one .metadata.json file is produced that contains all the metadata
|
||||
// necessary for symbols exported from the library index.
|
||||
// In the generated .ngfactory.ts files flat module index is used to import symbols
|
||||
// includes both the public API from the library index as well as shrowded internal
|
||||
// symbols.
|
||||
// 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 supplied
|
||||
// an error is produced.
|
||||
// A flat module index .d.ts and .js will be created with the given `flatModuleOutFile`
|
||||
// name in the same location as the library index .d.ts file is emitted.
|
||||
// For example, if a library uses `public_api.ts` file as the library index of the
|
||||
// module the `tsconfig.json` `files` field would be `["public_api.ts"]`. The
|
||||
// `flatModuleOutFile` options could then be set to, for example `"index.js"`, which
|
||||
// produces `index.d.ts` and `index.metadata.json` files. The library's
|
||||
// `package.json`'s `module` field would be `"index.js"` and the `typings` field would
|
||||
// be `"index.d.ts"`.
|
||||
flatModuleOutFile?: string;
|
||||
|
||||
// Preferred module id to use for importing flat module. References generated by `ngc`
|
||||
// will use this module name when importing symbols from the flat module. This is only
|
||||
// meaningful when `flatModuleOutFile` is also supplied. It is otherwise ignored.
|
||||
flatModuleId?: string;
|
||||
|
||||
// Whether to generate code for library code.
|
||||
// If true, produce .ngfactory.ts and .ngstyle.ts files for .d.ts inputs.
|
||||
// Default is true.
|
||||
generateCodeForLibraries?: boolean;
|
||||
|
||||
// Insert JSDoc type annotations needed by Closure Compiler
|
||||
annotateForClosureCompiler?: boolean;
|
||||
|
||||
// Modify how angular annotations are emitted to improve tree-shaking.
|
||||
// Default is static fields.
|
||||
// decorators: Leave the Decorators in-place. This makes compilation faster.
|
||||
// TypeScript will emit calls to the __decorate helper.
|
||||
// `--emitDecoratorMetadata` can be used for runtime reflection.
|
||||
// However, the resulting code will not properly tree-shake.
|
||||
// static fields: Replace decorators with a static field in the class.
|
||||
// Allows advanced tree-shakers like Closure Compiler to remove
|
||||
// unused classes.
|
||||
annotationsAs?: 'decorators'|'static fields';
|
||||
|
||||
// Print extra information while running the compiler
|
||||
trace?: boolean;
|
||||
|
||||
/** @deprecated since v4 this option has no effect anymore. */
|
||||
debug?: boolean;
|
||||
|
||||
// Whether to enable support for <template> and the template attribute (true by default)
|
||||
enableLegacyTemplate?: boolean;
|
||||
|
||||
// Whether to generate .ngsummary.ts files that allow to use AOTed artifacts
|
||||
// in JIT mode. This is off by default.
|
||||
enableSummariesForJit?: boolean;
|
||||
|
||||
// Whether to compile generated .ngfacgtory.ts files, even when they are no
|
||||
// matched by the `files` / `includes` in the `tsconfig.json`.
|
||||
// This is off by default.
|
||||
alwaysCompileGeneratedCode?: boolean;
|
||||
}
|
||||
|
||||
export default Options;
|
284
packages/tsc-wrapped/src/schema.ts
Normal file
284
packages/tsc-wrapped/src/schema.ts
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// Metadata Schema
|
||||
|
||||
// If you make a backwards incompatible change to the schema, increment the VERSION number.
|
||||
|
||||
// If you make a backwards compatible change to the metadata (such as adding an option field) then
|
||||
// leave VERSION the same. If possible, as many versions of the metadata that can represent the
|
||||
// semantics of the file in an array. For example, when generating a version 2 file, if version 1
|
||||
// can accurately represent the metadata, generate both version 1 and version 2 in an array.
|
||||
|
||||
export const VERSION = 3;
|
||||
|
||||
export type MetadataEntry = ClassMetadata | InterfaceMetadata | FunctionMetadata | MetadataValue;
|
||||
|
||||
export interface ModuleMetadata {
|
||||
__symbolic: 'module';
|
||||
version: number;
|
||||
exports?: ModuleExportMetadata[];
|
||||
importAs?: string;
|
||||
metadata: {[name: string]: MetadataEntry};
|
||||
origins?: {[name: string]: string};
|
||||
}
|
||||
export function isModuleMetadata(value: any): value is ModuleMetadata {
|
||||
return value && value.__symbolic === 'module';
|
||||
}
|
||||
|
||||
export interface ModuleExportMetadata {
|
||||
export?: (string|{name: string, as: string})[];
|
||||
from: string;
|
||||
}
|
||||
|
||||
export interface ClassMetadata {
|
||||
__symbolic: 'class';
|
||||
extends?: MetadataSymbolicExpression|MetadataError;
|
||||
arity?: number;
|
||||
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
||||
members?: MetadataMap;
|
||||
statics?: {[name: string]: MetadataValue | FunctionMetadata};
|
||||
}
|
||||
export function isClassMetadata(value: any): value is ClassMetadata {
|
||||
return value && value.__symbolic === 'class';
|
||||
}
|
||||
|
||||
export interface InterfaceMetadata { __symbolic: 'interface'; }
|
||||
export function isInterfaceMetadata(value: any): value is InterfaceMetadata {
|
||||
return value && value.__symbolic === 'interface';
|
||||
}
|
||||
|
||||
export interface MetadataMap { [name: string]: MemberMetadata[]; }
|
||||
|
||||
export interface MemberMetadata {
|
||||
__symbolic: 'constructor'|'method'|'property';
|
||||
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
||||
}
|
||||
export function isMemberMetadata(value: any): value is MemberMetadata {
|
||||
if (value) {
|
||||
switch (value.__symbolic) {
|
||||
case 'constructor':
|
||||
case 'method':
|
||||
case 'property':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface MethodMetadata extends MemberMetadata {
|
||||
__symbolic: 'constructor'|'method';
|
||||
parameterDecorators?: (MetadataSymbolicExpression|MetadataError)[][];
|
||||
}
|
||||
export function isMethodMetadata(value: any): value is MethodMetadata {
|
||||
return value && (value.__symbolic === 'constructor' || value.__symbolic === 'method');
|
||||
}
|
||||
|
||||
export interface ConstructorMetadata extends MethodMetadata {
|
||||
__symbolic: 'constructor';
|
||||
parameters?: (MetadataSymbolicExpression|MetadataError|null)[];
|
||||
}
|
||||
export function isConstructorMetadata(value: any): value is ConstructorMetadata {
|
||||
return value && value.__symbolic === 'constructor';
|
||||
}
|
||||
|
||||
export interface FunctionMetadata {
|
||||
__symbolic: 'function';
|
||||
parameters: string[];
|
||||
defaults?: MetadataValue[];
|
||||
value: MetadataValue;
|
||||
}
|
||||
export function isFunctionMetadata(value: any): value is FunctionMetadata {
|
||||
return value && value.__symbolic === 'function';
|
||||
}
|
||||
|
||||
export type MetadataValue = string | number | boolean | MetadataObject | MetadataArray |
|
||||
MetadataSymbolicExpression | MetadataError;
|
||||
|
||||
export interface MetadataObject { [name: string]: MetadataValue; }
|
||||
|
||||
export interface MetadataArray { [name: number]: MetadataValue; }
|
||||
|
||||
export interface MetadataSymbolicExpression {
|
||||
__symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select'|'spread'|'if';
|
||||
}
|
||||
export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression {
|
||||
if (value) {
|
||||
switch (value.__symbolic) {
|
||||
case 'binary':
|
||||
case 'call':
|
||||
case 'index':
|
||||
case 'new':
|
||||
case 'pre':
|
||||
case 'reference':
|
||||
case 'select':
|
||||
case 'spread':
|
||||
case 'if':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicBinaryExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'binary';
|
||||
operator: '&&'|'||'|'|'|'^'|'&'|'=='|'!='|'==='|'!=='|'<'|'>'|'<='|'>='|'instanceof'|'in'|'as'|
|
||||
'<<'|'>>'|'>>>'|'+'|'-'|'*'|'/'|'%'|'**';
|
||||
left: MetadataValue;
|
||||
right: MetadataValue;
|
||||
}
|
||||
export function isMetadataSymbolicBinaryExpression(value: any):
|
||||
value is MetadataSymbolicBinaryExpression {
|
||||
return value && value.__symbolic === 'binary';
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicIndexExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'index';
|
||||
expression: MetadataValue;
|
||||
index: MetadataValue;
|
||||
}
|
||||
export function isMetadataSymbolicIndexExpression(value: any):
|
||||
value is MetadataSymbolicIndexExpression {
|
||||
return value && value.__symbolic === 'index';
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicCallExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'call'|'new';
|
||||
expression: MetadataValue;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataSymbolicCallExpression(value: any):
|
||||
value is MetadataSymbolicCallExpression {
|
||||
return value && (value.__symbolic === 'call' || value.__symbolic === 'new');
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicPrefixExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'pre';
|
||||
operator: '+'|'-'|'~'|'!';
|
||||
operand: MetadataValue;
|
||||
}
|
||||
export function isMetadataSymbolicPrefixExpression(value: any):
|
||||
value is MetadataSymbolicPrefixExpression {
|
||||
return value && value.__symbolic === 'pre';
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicIfExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'if';
|
||||
condition: MetadataValue;
|
||||
thenExpression: MetadataValue;
|
||||
elseExpression: MetadataValue;
|
||||
}
|
||||
export function isMetadataSymbolicIfExpression(value: any): value is MetadataSymbolicIfExpression {
|
||||
return value && value.__symbolic === 'if';
|
||||
}
|
||||
|
||||
export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
name: string;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataGlobalReferenceExpression(value: any):
|
||||
value is MetadataGlobalReferenceExpression {
|
||||
return value && value.name && !value.module && isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export interface MetadataModuleReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
}
|
||||
export function isMetadataModuleReferenceExpression(value: any):
|
||||
value is MetadataModuleReferenceExpression {
|
||||
return value && value.module && !value.name && !value.default &&
|
||||
isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export interface MetadataImportedSymbolReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
name: string;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataImportedSymbolReferenceExpression(value: any):
|
||||
value is MetadataImportedSymbolReferenceExpression {
|
||||
return value && value.module && !!value.name && isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export interface MetadataImportedDefaultReferenceExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'reference';
|
||||
module: string;
|
||||
default:
|
||||
boolean;
|
||||
arguments?: MetadataValue[];
|
||||
}
|
||||
export function isMetadataImportDefaultReference(value: any):
|
||||
value is MetadataImportedDefaultReferenceExpression {
|
||||
return value.module && value.default && isMetadataSymbolicReferenceExpression(value);
|
||||
}
|
||||
|
||||
export type MetadataSymbolicReferenceExpression = MetadataGlobalReferenceExpression |
|
||||
MetadataModuleReferenceExpression | MetadataImportedSymbolReferenceExpression |
|
||||
MetadataImportedDefaultReferenceExpression;
|
||||
export function isMetadataSymbolicReferenceExpression(value: any):
|
||||
value is MetadataSymbolicReferenceExpression {
|
||||
return value && value.__symbolic === 'reference';
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicSelectExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'select';
|
||||
expression: MetadataValue;
|
||||
name: string;
|
||||
}
|
||||
export function isMetadataSymbolicSelectExpression(value: any):
|
||||
value is MetadataSymbolicSelectExpression {
|
||||
return value && value.__symbolic === 'select';
|
||||
}
|
||||
|
||||
export interface MetadataSymbolicSpreadExpression extends MetadataSymbolicExpression {
|
||||
__symbolic: 'spread';
|
||||
expression: MetadataValue;
|
||||
}
|
||||
export function isMetadataSymbolicSpreadExpression(value: any):
|
||||
value is MetadataSymbolicSpreadExpression {
|
||||
return value && value.__symbolic === 'spread';
|
||||
}
|
||||
|
||||
export interface MetadataError {
|
||||
__symbolic: 'error';
|
||||
|
||||
/**
|
||||
* This message should be short and relatively discriptive and should be fixed once it is created.
|
||||
* If the reader doesn't recognize the message, it will display the message unmodified. If the
|
||||
* reader recognizes the error message is it free to use substitute message the is more
|
||||
* descriptive and/or localized.
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The line number of the error in the .ts file the metadata was created for.
|
||||
*/
|
||||
line?: number;
|
||||
|
||||
/**
|
||||
* The number of utf8 code-units from the beginning of the file of the error.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
context?: {[name: string]: string};
|
||||
}
|
||||
export function isMetadataError(value: any): value is MetadataError {
|
||||
return value && value.__symbolic === 'error';
|
||||
}
|
127
packages/tsc-wrapped/src/symbols.ts
Normal file
127
packages/tsc-wrapped/src/symbols.ts
Normal file
@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @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 ts from 'typescript';
|
||||
|
||||
import {MetadataSymbolicReferenceExpression, MetadataValue} from './schema';
|
||||
|
||||
export class Symbols {
|
||||
private _symbols: Map<string, MetadataValue>;
|
||||
private references = new Map<string, MetadataSymbolicReferenceExpression>();
|
||||
|
||||
constructor(private sourceFile: ts.SourceFile) {}
|
||||
|
||||
resolve(name: string, preferReference?: boolean): MetadataValue|undefined {
|
||||
return (preferReference && this.references.get(name)) || this.symbols.get(name);
|
||||
}
|
||||
|
||||
define(name: string, value: MetadataValue) { this.symbols.set(name, value); }
|
||||
defineReference(name: string, value: MetadataSymbolicReferenceExpression) {
|
||||
this.references.set(name, value);
|
||||
}
|
||||
|
||||
has(name: string): boolean { return this.symbols.has(name); }
|
||||
|
||||
private get symbols(): Map<string, MetadataValue> {
|
||||
let result = this._symbols;
|
||||
if (!result) {
|
||||
result = this._symbols = new Map<string, MetadataValue>();
|
||||
populateBuiltins(result);
|
||||
this.buildImports();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private buildImports(): void {
|
||||
const symbols = this._symbols;
|
||||
// Collect the imported symbols into this.symbols
|
||||
const stripQuotes = (s: string) => s.replace(/^['"]|['"]$/g, '');
|
||||
const visit = (node: ts.Node) => {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.ImportEqualsDeclaration:
|
||||
const importEqualsDeclaration = <ts.ImportEqualsDeclaration>node;
|
||||
if (importEqualsDeclaration.moduleReference.kind ===
|
||||
ts.SyntaxKind.ExternalModuleReference) {
|
||||
const externalReference =
|
||||
<ts.ExternalModuleReference>importEqualsDeclaration.moduleReference;
|
||||
// An `import <identifier> = require(<module-specifier>);
|
||||
if (!externalReference.expression.parent) {
|
||||
// The `parent` field of a node is set by the TypeScript binder (run as
|
||||
// part of the type checker). Setting it here allows us to call `getText()`
|
||||
// even if the `SourceFile` was not type checked (which looks for `SourceFile`
|
||||
// in the parent chain). This doesn't damage the node as the binder unconditionally
|
||||
// sets the parent.
|
||||
externalReference.expression.parent = externalReference;
|
||||
externalReference.parent = this.sourceFile as any;
|
||||
}
|
||||
const from = stripQuotes(externalReference.expression.getText());
|
||||
symbols.set(importEqualsDeclaration.name.text, {__symbolic: 'reference', module: from});
|
||||
} else {
|
||||
symbols.set(
|
||||
importEqualsDeclaration.name.text,
|
||||
{__symbolic: 'error', message: `Unsupported import syntax`});
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.ImportDeclaration:
|
||||
const importDecl = <ts.ImportDeclaration>node;
|
||||
if (!importDecl.importClause) {
|
||||
// An `import <module-specifier>` clause which does not bring symbols into scope.
|
||||
break;
|
||||
}
|
||||
if (!importDecl.moduleSpecifier.parent) {
|
||||
// See note above in the `ImportEqualDeclaration` case.
|
||||
importDecl.moduleSpecifier.parent = importDecl;
|
||||
importDecl.parent = this.sourceFile;
|
||||
}
|
||||
const from = stripQuotes(importDecl.moduleSpecifier.getText());
|
||||
if (importDecl.importClause.name) {
|
||||
// An `import <identifier> form <module-specifier>` clause. Record the defualt symbol.
|
||||
symbols.set(
|
||||
importDecl.importClause.name.text,
|
||||
{__symbolic: 'reference', module: from, default: true});
|
||||
}
|
||||
const bindings = importDecl.importClause.namedBindings;
|
||||
if (bindings) {
|
||||
switch (bindings.kind) {
|
||||
case ts.SyntaxKind.NamedImports:
|
||||
// An `import { [<identifier> [, <identifier>] } from <module-specifier>` clause
|
||||
for (const binding of (<ts.NamedImports>bindings).elements) {
|
||||
symbols.set(binding.name.text, {
|
||||
__symbolic: 'reference',
|
||||
module: from,
|
||||
name: binding.propertyName ? binding.propertyName.text : binding.name.text
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ts.SyntaxKind.NamespaceImport:
|
||||
// An `input * as <identifier> from <module-specifier>` clause.
|
||||
symbols.set(
|
||||
(<ts.NamespaceImport>bindings).name.text,
|
||||
{__symbolic: 'reference', module: from});
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
if (this.sourceFile) {
|
||||
ts.forEachChild(this.sourceFile, visit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateBuiltins(symbols: Map<string, MetadataValue>) {
|
||||
// From lib.core.d.ts (all "define const")
|
||||
['Object', 'Function', 'String', 'Number', 'Array', 'Boolean', 'Map', 'NaN', 'Infinity', 'Math',
|
||||
'Date', 'RegExp', 'Error', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError',
|
||||
'TypeError', 'URIError', 'JSON', 'ArrayBuffer', 'DataView', 'Int8Array', 'Uint8Array',
|
||||
'Uint8ClampedArray', 'Uint16Array', 'Int16Array', 'Int32Array', 'Uint32Array', 'Float32Array',
|
||||
'Float64Array']
|
||||
.forEach(name => symbols.set(name, {__symbolic: 'reference', name}));
|
||||
}
|
179
packages/tsc-wrapped/src/tsc.ts
Normal file
179
packages/tsc-wrapped/src/tsc.ts
Normal file
@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @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 {existsSync} from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import AngularCompilerOptions from './options';
|
||||
import {VinylFile, isVinylFile} from './vinyl_file';
|
||||
|
||||
/**
|
||||
* Our interface to the TypeScript standard compiler.
|
||||
* If you write an Angular compiler plugin for another build tool,
|
||||
* you should implement a similar interface.
|
||||
*/
|
||||
export interface CompilerInterface {
|
||||
readConfiguration(
|
||||
project: string|VinylFile, basePath: string, existingOptions?: ts.CompilerOptions):
|
||||
{parsed: ts.ParsedCommandLine, ngOptions: AngularCompilerOptions};
|
||||
typeCheck(compilerHost: ts.CompilerHost, program: ts.Program): void;
|
||||
emit(program: ts.Program): number;
|
||||
}
|
||||
|
||||
export class UserError extends Error {
|
||||
private _nativeError: Error;
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
// Required for TS 2.1, see
|
||||
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
Object.setPrototypeOf(this, UserError.prototype);
|
||||
|
||||
const nativeError = new Error(message) as any as Error;
|
||||
this._nativeError = nativeError;
|
||||
}
|
||||
|
||||
get message() { return this._nativeError.message; }
|
||||
set message(message) {
|
||||
if (this._nativeError) this._nativeError.message = message;
|
||||
}
|
||||
get name() { return this._nativeError.name; }
|
||||
set name(name) {
|
||||
if (this._nativeError) this._nativeError.name = name;
|
||||
}
|
||||
get stack() { return (this._nativeError as any).stack; }
|
||||
set stack(value) {
|
||||
if (this._nativeError) (this._nativeError as any).stack = value;
|
||||
}
|
||||
toString() { return this._nativeError.toString(); }
|
||||
}
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
function debug(msg: string, ...o: any[]) {
|
||||
// tslint:disable-next-line:no-console
|
||||
if (DEBUG) console.log(msg, ...o);
|
||||
}
|
||||
|
||||
export function formatDiagnostics(diags: ts.Diagnostic[]): string {
|
||||
return diags
|
||||
.map((d) => {
|
||||
let res = ts.DiagnosticCategory[d.category];
|
||||
if (d.file) {
|
||||
res += ' at ' + d.file.fileName + ':';
|
||||
const {line, character} = d.file.getLineAndCharacterOfPosition(d.start);
|
||||
res += (line + 1) + ':' + (character + 1) + ':';
|
||||
}
|
||||
res += ' ' + ts.flattenDiagnosticMessageText(d.messageText, '\n');
|
||||
return res;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function check(diags: ts.Diagnostic[]) {
|
||||
if (diags && diags.length && diags[0]) {
|
||||
throw new UserError(formatDiagnostics(diags));
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAngularCompilerOptions(options: AngularCompilerOptions): ts.Diagnostic[] {
|
||||
if (options.annotationsAs) {
|
||||
switch (options.annotationsAs) {
|
||||
case 'decorators':
|
||||
case 'static fields':
|
||||
break;
|
||||
default:
|
||||
return [{
|
||||
file: null,
|
||||
start: null,
|
||||
length: null,
|
||||
messageText:
|
||||
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 0
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Tsc implements CompilerInterface {
|
||||
private parseConfigHost: ts.ParseConfigHost;
|
||||
|
||||
constructor(private readFile = ts.sys.readFile, private readDirectory = ts.sys.readDirectory) {
|
||||
this.parseConfigHost = {
|
||||
useCaseSensitiveFileNames: true,
|
||||
fileExists: existsSync,
|
||||
readDirectory: this.readDirectory,
|
||||
readFile: ts.sys.readFile
|
||||
};
|
||||
}
|
||||
|
||||
readConfiguration(
|
||||
project: string|VinylFile, basePath: string, existingOptions?: ts.CompilerOptions) {
|
||||
// Allow a directory containing tsconfig.json as the project value
|
||||
// Note, TS@next returns an empty array, while earlier versions throw
|
||||
try {
|
||||
if (!isVinylFile(project) && this.readDirectory(project).length > 0) {
|
||||
project = path.join(project, 'tsconfig.json');
|
||||
}
|
||||
} catch (e) {
|
||||
// Was not a directory, continue on assuming it's a file
|
||||
}
|
||||
|
||||
let {config, error} = (() => {
|
||||
// project is vinyl like file object
|
||||
if (isVinylFile(project)) {
|
||||
return {config: JSON.parse(project.contents.toString()), error: null};
|
||||
}
|
||||
// project is path to project file
|
||||
else {
|
||||
return ts.readConfigFile(project, this.readFile);
|
||||
}
|
||||
})();
|
||||
check([error]);
|
||||
|
||||
const parsed =
|
||||
ts.parseJsonConfigFileContent(config, this.parseConfigHost, basePath, existingOptions);
|
||||
|
||||
check(parsed.errors);
|
||||
|
||||
// Default codegen goes to the current directory
|
||||
// Parsed options are already converted to absolute paths
|
||||
const ngOptions = config.angularCompilerOptions || {};
|
||||
ngOptions.genDir = path.join(basePath, ngOptions.genDir || '.');
|
||||
for (const key of Object.keys(parsed.options)) {
|
||||
ngOptions[key] = parsed.options[key];
|
||||
}
|
||||
check(validateAngularCompilerOptions(ngOptions));
|
||||
|
||||
return {parsed, ngOptions};
|
||||
}
|
||||
|
||||
typeCheck(compilerHost: ts.CompilerHost, program: ts.Program): void {
|
||||
debug('Checking global diagnostics...');
|
||||
check(program.getGlobalDiagnostics());
|
||||
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
debug('Type checking...');
|
||||
|
||||
for (const sf of program.getSourceFiles()) {
|
||||
diagnostics.push(...ts.getPreEmitDiagnostics(program, sf));
|
||||
}
|
||||
check(diagnostics);
|
||||
}
|
||||
|
||||
emit(program: ts.Program): number {
|
||||
debug('Emitting outputs...');
|
||||
const emitResult = program.emit();
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
diagnostics.push(...emitResult.diagnostics);
|
||||
return emitResult.emitSkipped ? 1 : 0;
|
||||
}
|
||||
}
|
||||
export const tsc: CompilerInterface = new Tsc();
|
18
packages/tsc-wrapped/src/vinyl_file.ts
Normal file
18
packages/tsc-wrapped/src/vinyl_file.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
export interface VinylFile extends Object {
|
||||
// Absolute path to the virtual file
|
||||
path: string;
|
||||
|
||||
// Content of the virtual file
|
||||
contents: Buffer;
|
||||
}
|
||||
|
||||
export function isVinylFile(obj: any): obj is VinylFile {
|
||||
return (typeof obj === 'object') && ('path' in obj) && ('contents' in obj);
|
||||
}
|
Reference in New Issue
Block a user