feat(ivy): support getting the corresponding `.d.ts` file in ngcc (#25406)
PR Close #25406
This commit is contained in:
parent
cdb0215d0b
commit
d6e91ba545
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -14,9 +14,9 @@ export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol)
|
|||
}
|
||||
|
||||
export function isDefined<T>(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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue