From d6e91ba545ca32343185a25634547032b89e7c6e Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Mon, 30 Jul 2018 15:23:22 +0300 Subject: [PATCH] feat(ivy): support getting the corresponding `.d.ts` file in ngcc (#25406) PR Close #25406 --- .../src/ngcc/src/parsing/utils.ts | 80 ++++++++++++++--- .../ngcc/src/transform/package_transformer.ts | 13 +-- packages/compiler-cli/src/ngcc/src/utils.ts | 4 +- .../src/ngcc/test/parsing/utils_spec.ts | 90 ++++++++++++++++--- 4 files changed, 154 insertions(+), 33 deletions(-) diff --git a/packages/compiler-cli/src/ngcc/src/parsing/utils.ts b/packages/compiler-cli/src/ngcc/src/parsing/utils.ts index e9270e0d85..f350df220b 100644 --- a/packages/compiler-cli/src/ngcc/src/parsing/utils.ts +++ b/packages/compiler-cli/src/ngcc/src/parsing/utils.ts @@ -7,11 +7,63 @@ */ import {readFileSync} from 'fs'; -import {dirname, resolve} from 'path'; +import {dirname, relative, resolve} from 'path'; import {find} from 'shelljs'; +import {isDefined} from '../utils'; + /** - * Match paths to package.json files. + * Represents an entry point to a package or sub-package. + * + * It exposes the absolute path to the entry point file and a method to get the `.d.ts` file that + * corresponds to any source file that belongs to the package (assuming source files and `.d.ts` + * files have the same directory layout). + */ +export class EntryPoint { + entryFileName: string; + private entryRoot: string; + private dtsEntryRoot?: string; + + /** + * @param packageRoot The absolute path to the root directory that contains the package. + * @param relativeEntryPath The relative path to the entry point file. + * @param relativeDtsEntryPath The relative path to the `.d.ts` entry point file. + */ + constructor(packageRoot: string, relativeEntryPath: string, relativeDtsEntryPath?: string) { + this.entryFileName = resolve(packageRoot, relativeEntryPath); + this.entryRoot = dirname(this.entryFileName); + + if (isDefined(relativeDtsEntryPath)) { + const dtsEntryFileName = resolve(packageRoot, relativeDtsEntryPath); + this.dtsEntryRoot = dirname(dtsEntryFileName); + } + } + + /** + * Given the absolute path to a source file, return the absolute path to the corresponding `.d.ts` + * file. Assume that source files and `.d.ts` files have the same directory layout and the names + * of the `.d.ts` files can be derived by replacing the `.js` extension of the source file with + * `.d.ts`. + * + * @param sourceFileName The absolute path to the source file whose corresponding `.d.ts` file + * should be returned. + * + * @returns The absolute path to the `.d.ts` file that corresponds to the specified source file. + */ + getDtsFileNameFor(sourceFileName: string): string { + if (!isDefined(this.dtsEntryRoot)) { + throw new Error('No `.d.ts` entry path was specified.'); + } + + const relativeSourcePath = relative(this.entryRoot, sourceFileName); + const dtsFileName = resolve(this.dtsEntryRoot, relativeSourcePath).replace(/\.js$/, '.d.ts'); + + return dtsFileName; + } +} + +/** + * Match paths to `package.json` files. */ const PACKAGE_JSON_REGEX = /\/package\.json$/; @@ -21,9 +73,10 @@ const PACKAGE_JSON_REGEX = /\/package\.json$/; 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. + * Search the `rootDirectory` and its subdirectories to find `package.json` files. + * It ignores node dependencies, i.e. those under `node_modules` directories. + * + * @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 @@ -36,17 +89,22 @@ export function findAllPackageJsonFiles(rootDirectory: string): string[] { /** * 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. + * + * @param packageDirectory The absolute path to the root directory that contains the package. + * @param format The format of the entry points to look for within the package. + * + * @returns A collection of `EntryPoint`s that correspond to entry points for the package. */ -export function getEntryPoints(packageDirectory: string, format: string): string[] { +export function getEntryPoints(packageDirectory: string, format: string): EntryPoint[] { 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); + const relativeDtsEntryPointPath = entryPointPackageJson.typings; + return relativeEntryPointPath && + new EntryPoint( + dirname(packageJsonPath), relativeEntryPointPath, relativeDtsEntryPointPath); }) - .filter(entryPointPath => entryPointPath); + .filter(entryPoint => entryPoint); } diff --git a/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts b/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts index 044b0a1bbc..22f45764fb 100644 --- a/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/transform/package_transformer.ts @@ -44,24 +44,25 @@ export class PackageTransformer { transform(packagePath: string, format: string): void { const sourceNodeModules = this.findNodeModulesPath(packagePath); const targetNodeModules = sourceNodeModules.replace(/node_modules$/, 'node_modules_ngtsc'); - const entryPointPaths = getEntryPoints(packagePath, format); - entryPointPaths.forEach(entryPointPath => { + const entryPoints = getEntryPoints(packagePath, format); + + entryPoints.forEach(entryPoint => { const options: ts.CompilerOptions = { allowJs: true, maxNodeModuleJsDepth: Infinity, - rootDir: entryPointPath, + rootDir: entryPoint.entryFileName, }; const host = ts.createCompilerHost(options); - const packageProgram = ts.createProgram([entryPointPath], options, host); - const entryPointFile = packageProgram.getSourceFile(entryPointPath) !; + const packageProgram = ts.createProgram([entryPoint.entryFileName], options, host); const typeChecker = packageProgram.getTypeChecker(); - const reflectionHost = this.getHost(format, packageProgram); + const parser = this.getFileParser(format, packageProgram, reflectionHost); const analyzer = new Analyzer(typeChecker, reflectionHost); const renderer = this.getRenderer(format, packageProgram, reflectionHost); + const entryPointFile = packageProgram.getSourceFile(entryPoint.entryFileName) !; const parsedFiles = parser.parseFile(entryPointFile); parsedFiles.forEach(parsedFile => { const analyzedFile = analyzer.analyzeFile(parsedFile); diff --git a/packages/compiler-cli/src/ngcc/src/utils.ts b/packages/compiler-cli/src/ngcc/src/utils.ts index 425e6c9da7..7fe3aee36f 100644 --- a/packages/compiler-cli/src/ngcc/src/utils.ts +++ b/packages/compiler-cli/src/ngcc/src/utils.ts @@ -14,9 +14,9 @@ export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol) } export function isDefined(value: T | undefined | null): value is T { - return !!value; + return (value !== undefined) && (value !== null); } export function getNameText(name: ts.PropertyName | ts.BindingName): string { return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText(); -} \ No newline at end of file +} diff --git a/packages/compiler-cli/src/ngcc/test/parsing/utils_spec.ts b/packages/compiler-cli/src/ngcc/test/parsing/utils_spec.ts index 559fcea3f5..c3c6bedc17 100644 --- a/packages/compiler-cli/src/ngcc/test/parsing/utils_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/parsing/utils_spec.ts @@ -7,12 +7,16 @@ */ import * as mockFs from 'mock-fs'; -import {findAllPackageJsonFiles, getEntryPoints} from '../../src/parsing/utils'; +import {EntryPoint, findAllPackageJsonFiles, getEntryPoints} from '../../src/parsing/utils'; function createMockFileSystem() { mockFs({ '/node_modules/@angular/common': { - 'package.json': '{ "fesm2015": "./fesm2015/common.js", "fesm5": "./fesm5/common.js" }', + 'package.json': `{ + "fesm2015": "./fesm2015/common.js", + "fesm5": "./fesm5/common.js", + "typings": "./common.d.ts" + }`, 'fesm2015': { 'common.js': 'DUMMY CONTENT', 'http.js': 'DUMMY CONTENT', @@ -20,14 +24,28 @@ function createMockFileSystem() { 'testing.js': 'DUMMY CONTENT', }, 'http': { - 'package.json': '{ "fesm2015": "../fesm2015/http.js", "fesm5": "../fesm5/http.js" }', + 'package.json': `{ + "fesm2015": "../fesm2015/http.js", + "fesm5": "../fesm5/http.js", + "typings": "./http.d.ts" + }`, 'testing': { - 'package.json': - '{ "fesm2015": "../../fesm2015/http/testing.js", "fesm5": "../../fesm5/http/testing.js" }', + 'package.json': `{ + "fesm2015": "../../fesm2015/http/testing.js", + "fesm5": "../../fesm5/http/testing.js", + "no-typings": "for testing purposes" + }`, }, }, + 'other': { + 'package.json': '{ }', + }, 'testing': { - 'package.json': '{ "fesm2015": "../fesm2015/testing.js", "fesm5": "../fesm5/testing.js" }', + 'package.json': `{ + "fesm2015": "../fesm2015/testing.js", + "fesm5": "../fesm5/testing.js", + "no-typings": "for testing purposes" + }`, }, 'node_modules': { 'tslib': { @@ -63,6 +81,29 @@ function restoreRealFileSystem() { mockFs.restore(); } +describe('EntryPoint', () => { + it('should not break when called without a `relativeDtsEntryPath`', + () => { expect(() => new EntryPoint('/foo', './bar')).not.toThrow(); }); + + it('should expose the absolute path to the entry point file', () => { + const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js'); + expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js'); + }); + + describe('.getDtsFileNameFor()', () => { + it('should throw if no `.d.ts` entry path was specified', () => { + const entryPoint = new EntryPoint('/foo/bar', '../baz/qux.js'); + expect(() => entryPoint.getDtsFileNameFor('test')) + .toThrowError('No `.d.ts` entry path was specified.'); + }); + + it('should return the absolute path to the corresponding `.d.ts` file', () => { + const entryPoint = new EntryPoint('/foo/bar', '../src/entry.js', '../dts/entry.d.ts'); + expect(entryPoint.getDtsFileNameFor('/foo/src/qu/x.js')).toBe('/foo/dts/qu/x.d.ts'); + }); + }); +}); + describe('findAllPackageJsonFiles()', () => { beforeEach(createMockFileSystem); afterEach(restoreRealFileSystem); @@ -72,6 +113,7 @@ describe('findAllPackageJsonFiles()', () => { expect(paths.sort()).toEqual([ '/node_modules/@angular/common/http/package.json', '/node_modules/@angular/common/http/testing/package.json', + '/node_modules/@angular/common/other/package.json', '/node_modules/@angular/common/package.json', '/node_modules/@angular/common/testing/package.json', ]); @@ -102,9 +144,12 @@ 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([ + it('should return the entry points for the specified format from each `package.json`', () => { + const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm2015'); + entryPoints.forEach(ep => expect(ep).toEqual(jasmine.any(EntryPoint))); + + const sortedPaths = entryPoints.map(x => x.entryFileName).sort(); + expect(sortedPaths).toEqual([ '/node_modules/@angular/common/fesm2015/common.js', '/node_modules/@angular/common/fesm2015/http.js', '/node_modules/@angular/common/fesm2015/http/testing.js', @@ -112,13 +157,30 @@ describe('getEntryPoints()', () => { ]); }); - 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 `package.json` files', () => { + const entryPoints = getEntryPoints('/node_modules/@angular/other', 'fesm2015'); + expect(entryPoints).toEqual([]); }); it('should return an empty array if there are no matching formats', () => { - const paths = getEntryPoints('/node_modules/@angular/other', 'main'); - expect(paths).toEqual([]); + const entryPoints = getEntryPoints('/node_modules/@angular/common', 'fesm3000'); + expect(entryPoints).toEqual([]); + }); + + it('should return an entry point even if the typings are not specified', () => { + const entryPoints = getEntryPoints('/node_modules/@angular/common/http', 'fesm2015'); + const sortedEntryPoints = + entryPoints.sort((a, b) => (a.entryFileName > b.entryFileName) ? 1 : -1); + const sortedPaths = sortedEntryPoints.map(x => x.entryFileName); + + expect(sortedPaths).toEqual([ + '/node_modules/@angular/common/fesm2015/http.js', + '/node_modules/@angular/common/fesm2015/http/testing.js', + ]); + + expect(() => sortedEntryPoints[0].getDtsFileNameFor(sortedEntryPoints[0].entryFileName)) + .not.toThrow(); + expect(() => sortedEntryPoints[1].getDtsFileNameFor(sortedEntryPoints[1].entryFileName)) + .toThrow(); }); });