feat(ivy): implement esm2015 and esm5 file parsers (#24897)
PR Close #24897
This commit is contained in:
parent
45cf5b5dad
commit
2f70e90493
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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[], ) {}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue