feat(ivy): implement esm2015 and esm5 file parsers (#24897)

PR Close #24897
This commit is contained in:
Pete Bacon Darwin 2018-07-16 08:53:16 +01:00 committed by Igor Minar
parent 45cf5b5dad
commit 2f70e90493
9 changed files with 493 additions and 0 deletions

View File

@ -0,0 +1,54 @@
/**
* @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 {NgccReflectionHost} from '../host/ngcc_host';
import {getOriginalSymbol, isDefined} from '../utils';
import {FileParser} from './file_parser';
import {ParsedClass} from './parsed_class';
import {ParsedFile} from './parsed_file';
export class Esm2015FileParser implements FileParser {
checker = this.program.getTypeChecker();
constructor(protected program: ts.Program, protected host: NgccReflectionHost) {}
parseFile(file: ts.SourceFile): ParsedFile[] {
const moduleSymbol = this.checker.getSymbolAtLocation(file);
const map = new Map<ts.SourceFile, ParsedFile>();
if (moduleSymbol) {
const exportClasses = this.checker.getExportsOfModule(moduleSymbol)
.map(getOriginalSymbol(this.checker))
.filter(exportSymbol => exportSymbol.flags & ts.SymbolFlags.Class);
const classDeclarations = exportClasses.map(exportSymbol => exportSymbol.valueDeclaration)
.filter(isDefined)
.filter(ts.isClassDeclaration);
const decoratedClasses =
classDeclarations
.map(declaration => {
const decorators = this.host.getDecoratorsOfDeclaration(declaration);
return decorators && declaration.name &&
new ParsedClass(declaration.name.text, declaration, decorators);
})
.filter(isDefined);
decoratedClasses.forEach(clazz => {
const file = clazz.declaration.getSourceFile();
if (!map.has(file)) {
map.set(file, new ParsedFile(file));
}
map.get(file) !.decoratedClasses.push(clazz);
});
}
return Array.from(map.values());
}
}

View File

@ -0,0 +1,59 @@
/**
* @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 {NgccReflectionHost} from '../host/ngcc_host';
import {getNameText, getOriginalSymbol, isDefined} from '../utils';
import {FileParser} from './file_parser';
import {ParsedClass} from './parsed_class';
import {ParsedFile} from './parsed_file';
/**
* Parses ESM5 package files for decoratrs classes.
* ESM5 "classes" are actually functions wrapped by and returned
* from an IFEE.
*/
export class Esm5FileParser implements FileParser {
checker = this.program.getTypeChecker();
constructor(protected program: ts.Program, protected host: NgccReflectionHost) {}
parseFile(file: ts.SourceFile): ParsedFile[] {
const moduleSymbol = this.checker.getSymbolAtLocation(file);
const map = new Map<ts.SourceFile, ParsedFile>();
const getParsedClass = (declaration: ts.VariableDeclaration) => {
const decorators = this.host.getDecoratorsOfDeclaration(declaration);
if (decorators) {
return new ParsedClass(getNameText(declaration.name), declaration, decorators);
}
};
if (moduleSymbol) {
const classDeclarations = this.checker.getExportsOfModule(moduleSymbol)
.map(getOriginalSymbol(this.checker))
.map(exportSymbol => exportSymbol.valueDeclaration)
.filter(isDefined)
.filter(ts.isVariableDeclaration);
const decoratedClasses = classDeclarations.map(getParsedClass).filter(isDefined);
decoratedClasses.forEach(clazz => {
const file = clazz.declaration.getSourceFile();
if (!map.has(file)) {
map.set(file, new ParsedFile(file));
}
map.get(file) !.decoratedClasses.push(clazz);
});
}
return Array.from(map.values());
}
}

View File

@ -0,0 +1,35 @@
/**
* @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 {ParsedFile} from './parsed_file';
/**
* Classes that implement this interface can parse a file in a package to
* find the "declarations" (representing exported classes), that are decorated with core
* decorators, such as `@Component`, `@Injectable`, etc.
*
* Identifying classes can be different depending upon the format of the source file.
*
* For example:
*
* - ES2015 files contain `class Xxxx {...}` style declarations
* - ES5 files contain `var Xxxx = (function () { function Xxxx() { ... }; return Xxxx; })();` style
* declarations
* - UMD have similar declarations to ES5 files but the whole thing is wrapped in IIFE module
* wrapper
* function.
*/
export interface FileParser {
/**
* Parse a file to identify the decorated classes.
*
* @param file The the entry point file for identifying classes to process.
* @returns A `ParsedFiles` collection that holds the decorated classes and import information.
*/
parseFile(file: ts.SourceFile): ParsedFile[];
}

View File

@ -0,0 +1,26 @@
/**
* @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 {Decorator} from '../../../ngtsc/host';
/**
* A simple container that holds the details of a decorated class that has been
* parsed out of a package.
*/
export class ParsedClass {
/**
* Initialize a `DecoratedClass` that was found by parsing a package.
* @param name The name of the class that has been found. This is mostly used
* for informational purposes.
* @param declaration The TypeScript AST node where this class is declared
* @param decorators The collection of decorators that have been found on this class.
*/
constructor(
public name: string, public declaration: ts.Declaration, public decorators: Decorator[], ) {}
}

View File

@ -0,0 +1,23 @@
/**
* @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 {ParsedClass} from './parsed_class';
/**
* Information about a source file that has been parsed to
* extract all the decorated exported classes.
*/
export class ParsedFile {
/**
* The decorated exported classes that have been parsed out
* from the file.
*/
public decoratedClasses: ParsedClass[] = [];
constructor(public sourceFile: ts.SourceFile) {}
}

View File

@ -0,0 +1,52 @@
/**
* @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 {readFileSync} from 'fs';
import {dirname, resolve} from 'path';
import {find} from 'shelljs';
/**
* Match paths to package.json files.
*/
const PACKAGE_JSON_REGEX = /\/package\.json$/;
/**
* Match paths that have a `node_modules` segment at the start or in the middle.
*/
const NODE_MODULES_REGEX = /(?:^|\/)node_modules\//;
/**
* Search the `rootDirectory` and its subdirectories to find package.json files.
* It ignores node dependencies, i.e. those under `node_modules` folders.
* @param rootDirectory the directory in which we should search.
*/
export function findAllPackageJsonFiles(rootDirectory: string): string[] {
// TODO(gkalpak): Investigate whether skipping `node_modules/` directories (instead of traversing
// them and filtering out the results later) makes a noticeable difference.
const paths = Array.from(find(rootDirectory));
return paths.filter(
path => PACKAGE_JSON_REGEX.test(path) &&
!NODE_MODULES_REGEX.test(path.slice(rootDirectory.length)));
}
/**
* Identify the entry points of a package.
* @param packageDirectory The absolute path to the root directory that contains this package.
* @param format The format of the entry point within the package.
* @returns A collection of paths that point to entry points for this package.
*/
export function getEntryPoints(packageDirectory: string, format: string): string[] {
const packageJsonPaths = findAllPackageJsonFiles(packageDirectory);
return packageJsonPaths
.map(packageJsonPath => {
const entryPointPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const relativeEntryPointPath = entryPointPackageJson[format];
return relativeEntryPointPath && resolve(dirname(packageJsonPath), relativeEntryPointPath);
})
.filter(entryPointPath => entryPointPath);
}

View File

@ -0,0 +1,56 @@
/**
* @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 {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Esm2015FileParser} from '../../src/parsing/esm2015_parser';
import {makeProgram} from '../helpers/utils';
const BASIC_FILE = {
name: '/primary.js',
contents: `
class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
class B {}
B.decorators = [
{ type: Directive, args: [{ selector: '[b]' }] }
];
function x() {}
function y() {}
class C {}
export { A, x, C };
`
};
describe('Esm2015PackageParser', () => {
describe('getDecoratedClasses()', () => {
it('should return an array of object for each class that is exported and decorated', () => {
const program = makeProgram(BASIC_FILE);
const host = new Esm2015ReflectionHost(program.getTypeChecker());
const parser = new Esm2015FileParser(program, host);
const parsedFiles = parser.parseFile(program.getSourceFile(BASIC_FILE.name) !);
expect(parsedFiles.length).toEqual(1);
const decoratedClasses = parsedFiles[0].decoratedClasses;
expect(decoratedClasses.length).toEqual(1);
const decoratedClass = decoratedClasses[0];
expect(decoratedClass.name).toEqual('A');
expect(ts.isClassDeclaration(decoratedClass.declaration)).toBeTruthy();
expect(decoratedClass.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
});
});
});

View File

@ -0,0 +1,64 @@
/**
* @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 {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {Esm5FileParser} from '../../src/parsing/esm5_parser';
import {makeProgram} from '../helpers/utils';
const BASIC_FILE = {
name: '/primary.js',
contents: `
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
return A;
}());
var B = (function() {
function B() {}
B.decorators = [
{ type: Directive, args: [{ selector: '[b]' }] }
];
return B;
}());
function x() {}
function y() {}
var C = (function() {
function C() {}
return C;
});
export { A, x, C };
`
};
describe('Esm5FileParser', () => {
describe('getDecoratedClasses()', () => {
it('should return an array of object for each class that is exported and decorated', () => {
const program = makeProgram(BASIC_FILE);
const host = new Esm5ReflectionHost(program.getTypeChecker());
const parser = new Esm5FileParser(program, host);
const parsedFiles = parser.parseFile(program.getSourceFile(BASIC_FILE.name) !);
expect(parsedFiles.length).toEqual(1);
const decoratedClasses = parsedFiles[0].decoratedClasses;
expect(decoratedClasses.length).toEqual(1);
const decoratedClass = decoratedClasses[0];
expect(decoratedClass.name).toEqual('A');
expect(decoratedClass.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
});
});
});

View File

@ -0,0 +1,124 @@
/**
* @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 mockFs from 'mock-fs';
import {findAllPackageJsonFiles, getEntryPoints} from '../../src/parsing/utils';
function createMockFileSystem() {
mockFs({
'/node_modules/@angular/common': {
'package.json': '{ "fesm2015": "./fesm2015/common.js", "fesm5": "./fesm5/common.js" }',
'fesm2015': {
'common.js': 'DUMMY CONTENT',
'http.js': 'DUMMY CONTENT',
'http/testing.js': 'DUMMY CONTENT',
'testing.js': 'DUMMY CONTENT',
},
'http': {
'package.json': '{ "fesm2015": "../fesm2015/http.js", "fesm5": "../fesm5/http.js" }',
'testing': {
'package.json':
'{ "fesm2015": "../../fesm2015/http/testing.js", "fesm5": "../../fesm5/http/testing.js" }',
},
},
'testing': {
'package.json': '{ "fesm2015": "../fesm2015/testing.js", "fesm5": "../fesm5/testing.js" }',
},
'node_modules': {
'tslib': {
'package.json': '{ }',
'node_modules': {
'other-lib': {
'package.json': '{ }',
},
},
},
},
},
'/node_modules/@angular/other': {
'not-package.json': '{ "fesm2015": "./fesm2015/other.js" }',
'package.jsonot': '{ "fesm5": "./fesm5/other.js" }',
},
'/node_modules/@angular/other2': {
'node_modules_not': {
'lib1': {
'package.json': '{ }',
},
},
'not_node_modules': {
'lib2': {
'package.json': '{ }',
},
},
},
});
}
function restoreRealFileSystem() {
mockFs.restore();
}
describe('findAllPackageJsonFiles()', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
it('should find the `package.json` files below the specified directory', () => {
const paths = findAllPackageJsonFiles('/node_modules/@angular/common');
expect(paths.sort()).toEqual([
'/node_modules/@angular/common/http/package.json',
'/node_modules/@angular/common/http/testing/package.json',
'/node_modules/@angular/common/package.json',
'/node_modules/@angular/common/testing/package.json',
]);
});
it('should not find `package.json` files under `node_modules/`', () => {
const paths = findAllPackageJsonFiles('/node_modules/@angular/common');
expect(paths).not.toContain('/node_modules/@angular/common/node_modules/tslib/package.json');
expect(paths).not.toContain(
'/node_modules/@angular/common/node_modules/tslib/node_modules/other-lib/package.json');
});
it('should exactly match the name of `package.json` files', () => {
const paths = findAllPackageJsonFiles('/node_modules/@angular/other');
expect(paths).toEqual([]);
});
it('should exactly match the name of `node_modules/` directory', () => {
const paths = findAllPackageJsonFiles('/node_modules/@angular/other2');
expect(paths).toEqual([
'/node_modules/@angular/other2/node_modules_not/lib1/package.json',
'/node_modules/@angular/other2/not_node_modules/lib2/package.json',
]);
});
});
describe('getEntryPoints()', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
it('should return the paths for the specified format from each package.json', () => {
const paths = getEntryPoints('/node_modules/@angular/common', 'fesm2015');
expect(paths.sort()).toEqual([
'/node_modules/@angular/common/fesm2015/common.js',
'/node_modules/@angular/common/fesm2015/http.js',
'/node_modules/@angular/common/fesm2015/http/testing.js',
'/node_modules/@angular/common/fesm2015/testing.js',
]);
});
it('should return an empty array if there are no matching package.json files', () => {
const paths = getEntryPoints('/node_modules/@angular/other', 'fesm2015');
expect(paths).toEqual([]);
});
it('should return an empty array if there are no matching formats', () => {
const paths = getEntryPoints('/node_modules/@angular/other', 'main');
expect(paths).toEqual([]);
});
});