fix(ivy): ngcc should compile entry-points in the correct order (#25862)
The compiler should process all an entry-points dependencies before processing that entry-point. PR Close #25862
This commit is contained in:

committed by
Ben Lesh

parent
976389836e
commit
9b1bb370a3
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @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, readFileSync, writeFileSync} from 'fs';
|
||||
import * as mockFs from 'mock-fs';
|
||||
|
||||
import {checkMarkerFile, writeMarkerFile} from '../../src/packages/build_marker';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/node_modules/@angular/common': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/common.js",
|
||||
"fesm5": "./fesm5/common.js",
|
||||
"typings": "./common.d.ts"
|
||||
}`,
|
||||
'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",
|
||||
"typings": "./http.d.ts"
|
||||
}`,
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../../fesm2015/http/testing.js",
|
||||
"fesm5": "../../fesm5/http/testing.js",
|
||||
"typings": "../http/testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
'other': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../fesm2015/testing.js",
|
||||
"fesm5": "../fesm5/testing.js",
|
||||
"typings": "../testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
'node_modules': {
|
||||
'tslib': {
|
||||
'package.json': '{ }',
|
||||
'node_modules': {
|
||||
'other-lib': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/node_modules/@angular/no-typings': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/index.js"
|
||||
}`,
|
||||
'fesm2015': {
|
||||
'index.js': 'DUMMY CONTENT',
|
||||
'index.d.ts': 'DUMMY CONTENT',
|
||||
},
|
||||
},
|
||||
'/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();
|
||||
}
|
||||
|
||||
function createEntryPoint(path: string): EntryPoint {
|
||||
return {path, package: '', typings: ''};
|
||||
}
|
||||
|
||||
describe('Marker files', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
describe('writeMarkerFile', () => {
|
||||
it('should write a file containing the version placeholder', () => {
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(false);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
|
||||
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
|
||||
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'esm5');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkMarkerFile', () => {
|
||||
it('should return false if the marker file does not exist', () => {
|
||||
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the marker file exists and contains the correct version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
|
||||
'utf8');
|
||||
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should throw if the marker file exists but contains the wrong version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
|
||||
'utf8');
|
||||
expect(() => checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
|
||||
.toThrowError(
|
||||
'The ngcc compiler has changed since the last ngcc build.\n' +
|
||||
'Please completely remove `node_modules` and try again.');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* @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 'canonical-path';
|
||||
import * as mockFs from 'mock-fs';
|
||||
import * as ts from 'typescript';
|
||||
import {DependencyHost} from '../../src/packages/dependency_host';
|
||||
const Module = require('module');
|
||||
|
||||
interface DepMap {
|
||||
[path: string]: {resolved: string[], missing: string[]};
|
||||
}
|
||||
|
||||
describe('DependencyHost', () => {
|
||||
let host: DependencyHost;
|
||||
beforeEach(() => host = new DependencyHost());
|
||||
|
||||
describe('getDependencies()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should not generate a TS AST if the source does not contain any imports or re-exports',
|
||||
() => {
|
||||
spyOn(ts, 'createSourceFile');
|
||||
host.computeDependencies('/no/imports/or/re-exports.js', new Set(), new Set());
|
||||
expect(ts.createSourceFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should resolve all the external imports of the source file', () => {
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/external/imports.js', resolved, missing);
|
||||
expect(resolved.size).toBe(2);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
it('should resolve all the external re-exports of the source file', () => {
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/external/re-exports.js', resolved, missing);
|
||||
expect(resolved.size).toBe(2);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
it('should capture missing external imports', () => {
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake(
|
||||
(from: string, importPath: string) =>
|
||||
importPath === 'missing' ? null : `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/external/imports-missing.js', resolved, missing);
|
||||
expect(resolved.size).toBe(1);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(missing.size).toBe(1);
|
||||
expect(missing.has('missing'));
|
||||
});
|
||||
|
||||
it('should recurse into internal dependencies', () => {
|
||||
spyOn(host, 'resolveInternal')
|
||||
.and.callFake(
|
||||
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough();
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/internal/outer.js', resolved, missing);
|
||||
expect(getDependenciesSpy).toHaveBeenCalledWith('/internal/outer.js', resolved, missing);
|
||||
expect(getDependenciesSpy)
|
||||
.toHaveBeenCalledWith('/internal/inner.js', resolved, missing, jasmine.any(Set));
|
||||
expect(resolved.size).toBe(1);
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
|
||||
it('should handle circular internal dependencies', () => {
|
||||
spyOn(host, 'resolveInternal')
|
||||
.and.callFake(
|
||||
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
|
||||
spyOn(host, 'tryResolveExternal')
|
||||
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
|
||||
const resolved = new Set();
|
||||
const missing = new Set();
|
||||
host.computeDependencies('/internal/circular-a.js', resolved, missing);
|
||||
expect(resolved.size).toBe(2);
|
||||
expect(resolved.has('RESOLVED/path/to/x'));
|
||||
expect(resolved.has('RESOLVED/path/to/y'));
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/no/imports/or/re-exports.js': 'some text but no import-like statements',
|
||||
'/external/imports.js': `import {X} from 'path/to/x';\nimport {Y} from 'path/to/y';`,
|
||||
'/external/re-exports.js': `export {X} from 'path/to/x';\nexport {Y} from 'path/to/y';`,
|
||||
'/external/imports-missing.js': `import {X} from 'path/to/x';\nimport {Y} from 'missing';`,
|
||||
'/internal/outer.js': `import {X} from './inner';`,
|
||||
'/internal/inner.js': `import {Y} from 'path/to/y';`,
|
||||
'/internal/circular-a.js': `import {B} from './circular-b'; import {X} from 'path/to/x';`,
|
||||
'/internal/circular-b.js': `import {A} from './circular-a'; import {Y} from 'path/to/y';`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('resolveInternal', () => {
|
||||
it('should resolve the dependency via `Module._resolveFilename`', () => {
|
||||
spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
|
||||
const result = host.resolveInternal('/SOURCE/PATH/FILE', '../TARGET/PATH/FILE');
|
||||
expect(result).toEqual('RESOLVED_PATH');
|
||||
});
|
||||
|
||||
it('should first resolve the `to` on top of the `from` directory', () => {
|
||||
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
|
||||
host.resolveInternal('/SOURCE/PATH/FILE', '../TARGET/PATH/FILE');
|
||||
expect(resolveSpy)
|
||||
.toHaveBeenCalledWith('/SOURCE/TARGET/PATH/FILE', jasmine.any(Object), false, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryResolveExternal', () => {
|
||||
it('should call `tryResolve`, appending `package.json` to the target path', () => {
|
||||
const tryResolveSpy = spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED');
|
||||
host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH');
|
||||
expect(tryResolveSpy).toHaveBeenCalledWith('SOURCE_PATH', 'TARGET_PATH/package.json');
|
||||
});
|
||||
|
||||
it('should return the directory containing the result from `tryResolve', () => {
|
||||
spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED');
|
||||
expect(host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH')).toEqual('PATH/TO');
|
||||
});
|
||||
|
||||
it('should return null if `tryResolve` returns null', () => {
|
||||
spyOn(host, 'tryResolve').and.returnValue(null);
|
||||
expect(host.tryResolveExternal('SOURCE_PATH', 'TARGET_PATH')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryResolve()', () => {
|
||||
it('should resolve the dependency via `Module._resolveFilename`, passing the `from` path to the `paths` option',
|
||||
() => {
|
||||
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
|
||||
const result = host.tryResolve('SOURCE_PATH', 'TARGET_PATH');
|
||||
expect(resolveSpy).toHaveBeenCalledWith('TARGET_PATH', jasmine.any(Object), false, {
|
||||
paths: ['SOURCE_PATH']
|
||||
});
|
||||
expect(result).toEqual('RESOLVED_PATH');
|
||||
});
|
||||
|
||||
it('should return null if `Module._resolveFilename` throws an error', () => {
|
||||
const resolveSpy =
|
||||
spyOn(Module, '_resolveFilename').and.throwError(`Cannot find module 'TARGET_PATH'`);
|
||||
const result = host.tryResolve('SOURCE_PATH', 'TARGET_PATH');
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStringImportOrReexport', () => {
|
||||
it('should return true if the statement is an import', () => {
|
||||
expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";')))
|
||||
.toBe(true);
|
||||
expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";')))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the statement is a re-export', () => {
|
||||
expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";')))
|
||||
.toBe(true);
|
||||
expect(host.isStringImportOrReexport(createStatement('export * from "some/x";'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the statement is not an import or a re-export', () => {
|
||||
expect(host.isStringImportOrReexport(createStatement('class X {}'))).toBe(false);
|
||||
expect(host.isStringImportOrReexport(createStatement('export function foo() {}')))
|
||||
.toBe(false);
|
||||
expect(host.isStringImportOrReexport(createStatement('export const X = 10;'))).toBe(false);
|
||||
});
|
||||
|
||||
function createStatement(source: string) {
|
||||
return ts
|
||||
.createSourceFile('source.js', source, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS)
|
||||
.statements[0];
|
||||
}
|
||||
});
|
||||
|
||||
describe('hasImportOrReeportStatements', () => {
|
||||
it('should return true if there is an import statement', () => {
|
||||
expect(host.hasImportOrReeportStatements('import {X} from "some/x";')).toBe(true);
|
||||
expect(host.hasImportOrReeportStatements('import * as X from "some/x";')).toBe(true);
|
||||
expect(
|
||||
host.hasImportOrReeportStatements('blah blah\n\n import {X} from "some/x";\nblah blah'))
|
||||
.toBe(true);
|
||||
expect(host.hasImportOrReeportStatements('\t\timport {X} from "some/x";')).toBe(true);
|
||||
});
|
||||
it('should return true if there is a re-export statement', () => {
|
||||
expect(host.hasImportOrReeportStatements('export {X} from "some/x";')).toBe(true);
|
||||
expect(
|
||||
host.hasImportOrReeportStatements('blah blah\n\n export {X} from "some/x";\nblah blah'))
|
||||
.toBe(true);
|
||||
expect(host.hasImportOrReeportStatements('\t\texport {X} from "some/x";')).toBe(true);
|
||||
expect(host.hasImportOrReeportStatements(
|
||||
'blah blah\n\n export * from "@angular/core;\nblah blah'))
|
||||
.toBe(true);
|
||||
});
|
||||
it('should return false if there is no import nor re-export statement', () => {
|
||||
expect(host.hasImportOrReeportStatements('blah blah')).toBe(false);
|
||||
expect(host.hasImportOrReeportStatements('export function moo() {}')).toBe(false);
|
||||
expect(host.hasImportOrReeportStatements('Some text that happens to include the word import'))
|
||||
.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function restoreRealFileSystem() { mockFs.restore(); }
|
||||
});
|
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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 {DependencyHost} from '../../src/packages/dependency_host';
|
||||
import {DependencyResolver} from '../../src/packages/dependency_resolver';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
|
||||
describe('DepencencyResolver', () => {
|
||||
let host: DependencyHost;
|
||||
let resolver: DependencyResolver;
|
||||
beforeEach(() => {
|
||||
host = new DependencyHost();
|
||||
resolver = new DependencyResolver(host);
|
||||
});
|
||||
describe('sortEntryPointsByDependency()', () => {
|
||||
const first = { path: 'first', esm2015: 'first/index.ts' } as EntryPoint;
|
||||
const second = { path: 'second', esm2015: 'second/index.ts' } as EntryPoint;
|
||||
const third = { path: 'third', esm2015: 'third/index.ts' } as EntryPoint;
|
||||
const fourth = { path: 'fourth', esm2015: 'fourth/index.ts' } as EntryPoint;
|
||||
const fifth = { path: 'fifth', esm2015: 'fifth/index.ts' } as EntryPoint;
|
||||
|
||||
const dependencies = {
|
||||
'first/index.ts': {resolved: ['second', 'third', 'ignored-1'], missing: []},
|
||||
'second/index.ts': {resolved: ['third', 'fifth'], missing: []},
|
||||
'third/index.ts': {resolved: ['fourth', 'ignored-2'], missing: []},
|
||||
'fourth/index.ts': {resolved: ['fifth'], missing: []},
|
||||
'fifth/index.ts': {resolved: [], missing: []},
|
||||
};
|
||||
|
||||
it('should order the entry points by their dependency on each other', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies));
|
||||
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
|
||||
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
|
||||
});
|
||||
|
||||
it('should remove entry-points that have missing direct dependencies', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
|
||||
'first/index.ts': {resolved: [], missing: ['missing']},
|
||||
'second/index.ts': {resolved: [], missing: []},
|
||||
}));
|
||||
const result = resolver.sortEntryPointsByDependency([first, second]);
|
||||
expect(result.entryPoints).toEqual([second]);
|
||||
expect(result.invalidEntryPoints).toEqual([
|
||||
{entryPoint: first, missingDependencies: ['missing']},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove entry points that depended upon an invalid entry-point', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
|
||||
'first/index.ts': {resolved: ['second'], missing: []},
|
||||
'second/index.ts': {resolved: [], missing: ['missing']},
|
||||
'third/index.ts': {resolved: [], missing: []},
|
||||
}));
|
||||
// Note that we will process `first` before `second`, which has the missing dependency.
|
||||
const result = resolver.sortEntryPointsByDependency([first, second, third]);
|
||||
expect(result.entryPoints).toEqual([third]);
|
||||
expect(result.invalidEntryPoints).toEqual([
|
||||
{entryPoint: second, missingDependencies: ['missing']},
|
||||
{entryPoint: first, missingDependencies: ['missing']},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove entry points that will depend upon an invalid entry-point', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
|
||||
'first/index.ts': {resolved: ['second'], missing: []},
|
||||
'second/index.ts': {resolved: [], missing: ['missing']},
|
||||
'third/index.ts': {resolved: [], missing: []},
|
||||
}));
|
||||
// Note that we will process `first` after `second`, which has the missing dependency.
|
||||
const result = resolver.sortEntryPointsByDependency([second, first, third]);
|
||||
expect(result.entryPoints).toEqual([third]);
|
||||
expect(result.invalidEntryPoints).toEqual([
|
||||
{entryPoint: second, missingDependencies: ['missing']},
|
||||
{entryPoint: first, missingDependencies: ['second']},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error if the entry point does not have the esm2015 format', () => {
|
||||
expect(() => resolver.sortEntryPointsByDependency([{ path: 'first' } as EntryPoint]))
|
||||
.toThrowError(`Esm2015 format missing in 'first' entry-point.`);
|
||||
});
|
||||
|
||||
it('should capture any dependencies that were ignored', () => {
|
||||
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies));
|
||||
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
|
||||
expect(result.ignoredDependencies).toEqual([
|
||||
{entryPoint: first, dependencyPath: 'ignored-1'},
|
||||
{entryPoint: third, dependencyPath: 'ignored-2'},
|
||||
]);
|
||||
});
|
||||
|
||||
interface DepMap {
|
||||
[path: string]: {resolved: string[], missing: string[]};
|
||||
}
|
||||
|
||||
function createFakeComputeDependencies(dependencies: DepMap) {
|
||||
return (entryPoint: string, resolved: Set<string>, missing: Set<string>) => {
|
||||
dependencies[entryPoint].resolved.forEach(dep => resolved.add(dep));
|
||||
dependencies[entryPoint].missing.forEach(dep => missing.add(dep));
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @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 {DependencyHost} from '../../src/packages/dependency_host';
|
||||
import {DependencyResolver} from '../../src/packages/dependency_resolver';
|
||||
import {EntryPoint} from '../../src/packages/entry_point';
|
||||
import {EntryPointFinder} from '../../src/packages/entry_point_finder';
|
||||
|
||||
describe('findEntryPoints()', () => {
|
||||
let resolver: DependencyResolver;
|
||||
let finder: EntryPointFinder;
|
||||
beforeEach(() => {
|
||||
resolver = new DependencyResolver(new DependencyHost());
|
||||
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
|
||||
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []};
|
||||
});
|
||||
finder = new EntryPointFinder(resolver);
|
||||
});
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should find sub-entry-points within a package', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/sub_entry_points');
|
||||
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
|
||||
expect(entryPointPaths).toEqual([
|
||||
['/sub_entry_points/common', '/sub_entry_points/common'],
|
||||
['/sub_entry_points/common', '/sub_entry_points/common/http'],
|
||||
['/sub_entry_points/common', '/sub_entry_points/common/http/testing'],
|
||||
['/sub_entry_points/common', '/sub_entry_points/common/testing'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find packages inside a namespace', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/namespaced');
|
||||
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
|
||||
expect(entryPointPaths).toEqual([
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common'],
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common/http'],
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common/http/testing'],
|
||||
['/namespaced/@angular/common', '/namespaced/@angular/common/testing'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no packages', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/no_packages');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no valid entry-points', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/no_valid_entry_points');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ignore folders starting with .', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/dotted_folders');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should ignore folders that are symlinked', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/symlinked_folders');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle nested node_modules folders', () => {
|
||||
const {entryPoints} = finder.findEntryPoints('/nested_node_modules');
|
||||
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
|
||||
expect(entryPointPaths).toEqual([
|
||||
['/nested_node_modules/outer', '/nested_node_modules/outer'],
|
||||
// Note that the inner entry point does not get included as part of the outer package
|
||||
[
|
||||
'/nested_node_modules/outer/node_modules/inner',
|
||||
'/nested_node_modules/outer/node_modules/inner'
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/sub_entry_points': {
|
||||
'common': {
|
||||
'package.json': createPackageJson('common'),
|
||||
'common.metadata.json': 'metadata info',
|
||||
'http': {
|
||||
'package.json': createPackageJson('http'),
|
||||
'http.metadata.json': 'metadata info',
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
},
|
||||
'/namespaced': {
|
||||
'@angular': {
|
||||
'common': {
|
||||
'package.json': createPackageJson('common'),
|
||||
'common.metadata.json': 'metadata info',
|
||||
'http': {
|
||||
'package.json': createPackageJson('http'),
|
||||
'http.metadata.json': 'metadata info',
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
'testing': {
|
||||
'package.json': createPackageJson('testing'),
|
||||
'testing.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/no_packages': {'should_not_be_found': {}},
|
||||
'/no_valid_entry_points': {
|
||||
'some_package': {
|
||||
'package.json': '{}',
|
||||
},
|
||||
},
|
||||
'/dotted_folders': {
|
||||
'.common': {
|
||||
'package.json': createPackageJson('common'),
|
||||
'common.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
'/symlinked_folders': {
|
||||
'common': mockFs.symlink({path: '/sub_entry_points/common'}),
|
||||
},
|
||||
'/nested_node_modules': {
|
||||
'outer': {
|
||||
'package.json': createPackageJson('outer'),
|
||||
'outer.metadata.json': 'metadata info',
|
||||
'node_modules': {
|
||||
'inner': {
|
||||
'package.json': createPackageJson('inner'),
|
||||
'inner.metadata.json': 'metadata info',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
function restoreRealFileSystem() { mockFs.restore(); }
|
||||
});
|
||||
|
||||
function createPackageJson(packageName: string): string {
|
||||
const packageJson: any = {
|
||||
typings: `./${packageName}.d.ts`,
|
||||
fesm2015: `./fesm2015/${packageName}.js`,
|
||||
esm2015: `./esm2015/${packageName}.js`,
|
||||
fesm5: `./fesm2015/${packageName}.js`,
|
||||
esm5: `./esm2015/${packageName}.js`,
|
||||
main: `./bundles/${packageName}.umd.js`,
|
||||
};
|
||||
return JSON.stringify(packageJson);
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @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 {getEntryPointInfo} from '../../src/packages/entry_point';
|
||||
|
||||
|
||||
describe('getEntryPointInfo()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should return an object containing absolute paths to the formats of the specified entry-point',
|
||||
() => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/valid_entry_point');
|
||||
expect(entryPoint).toEqual({
|
||||
package: '/some_package',
|
||||
path: '/some_package/valid_entry_point',
|
||||
typings: `/some_package/valid_entry_point/valid_entry_point.d.ts`,
|
||||
fesm2015: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
|
||||
esm2015: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
|
||||
fesm5: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
|
||||
esm5: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
|
||||
umd: `/some_package/valid_entry_point/bundles/valid_entry_point.umd.js`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there is no package.json at the entry-point path', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_package_json');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no typings field in the package.json', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_typings');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no esm2015 field in the package.json', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_esm2015');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no metadata.json file next to the typing file', () => {
|
||||
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_metadata.json');
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/some_package': {
|
||||
'valid_entry_point': {
|
||||
'package.json': createPackageJson('valid_entry_point'),
|
||||
'valid_entry_point.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_package_json': {
|
||||
// no package.json!
|
||||
'missing_package_json.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_typings': {
|
||||
'package.json': createPackageJson('missing_typings', {exclude: 'typings'}),
|
||||
'missing_typings.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_esm2015': {
|
||||
'package.json': createPackageJson('missing_esm2015', {exclude: 'esm2015'}),
|
||||
'missing_esm2015.metadata.json': 'some meta data',
|
||||
},
|
||||
'missing_metadata': {
|
||||
'package.json': createPackageJson('missing_metadata'),
|
||||
// no metadata.json!
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreRealFileSystem() {
|
||||
mockFs.restore();
|
||||
}
|
||||
|
||||
function createPackageJson(packageName: string, {exclude}: {exclude?: string} = {}): string {
|
||||
const packageJson: any = {
|
||||
typings: `./${packageName}.d.ts`,
|
||||
fesm2015: `./fesm2015/${packageName}.js`,
|
||||
esm2015: `./esm2015/${packageName}.js`,
|
||||
fesm5: `./fesm2015/${packageName}.js`,
|
||||
esm5: `./esm2015/${packageName}.js`,
|
||||
main: `./bundles/${packageName}.umd.js`,
|
||||
};
|
||||
if (exclude) {
|
||||
delete packageJson[exclude];
|
||||
}
|
||||
return JSON.stringify(packageJson);
|
||||
}
|
@ -1,240 +0,0 @@
|
||||
/**
|
||||
* @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, readFileSync, writeFileSync} from 'fs';
|
||||
import * as mockFs from 'mock-fs';
|
||||
|
||||
import {EntryPoint, checkMarkerFile, findAllPackageJsonFiles, getEntryPoints, writeMarkerFile} from '../../src/transform/utils';
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/node_modules/@angular/common': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/common.js",
|
||||
"fesm5": "./fesm5/common.js",
|
||||
"typings": "./common.d.ts"
|
||||
}`,
|
||||
'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",
|
||||
"typings": "./http.d.ts"
|
||||
}`,
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../../fesm2015/http/testing.js",
|
||||
"fesm5": "../../fesm5/http/testing.js",
|
||||
"typings": "../http/testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
'other': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
'testing': {
|
||||
'package.json': `{
|
||||
"fesm2015": "../fesm2015/testing.js",
|
||||
"fesm5": "../fesm5/testing.js",
|
||||
"typings": "../testing.d.ts"
|
||||
}`,
|
||||
},
|
||||
'node_modules': {
|
||||
'tslib': {
|
||||
'package.json': '{ }',
|
||||
'node_modules': {
|
||||
'other-lib': {
|
||||
'package.json': '{ }',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/node_modules/@angular/no-typings': {
|
||||
'package.json': `{
|
||||
"fesm2015": "./fesm2015/index.js"
|
||||
}`,
|
||||
'fesm2015': {
|
||||
'index.js': 'DUMMY CONTENT',
|
||||
'index.d.ts': 'DUMMY CONTENT',
|
||||
},
|
||||
},
|
||||
'/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('EntryPoint', () => {
|
||||
it('should expose the absolute path to the entry point file', () => {
|
||||
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
|
||||
expect(entryPoint.entryFileName).toBe('/foo/baz/quux.js');
|
||||
});
|
||||
|
||||
it('should expose the package root for the entry point file', () => {
|
||||
const entryPoint = new EntryPoint('/foo/bar', '../baz/qux/../quux.js', '/typings/foo/bar.d.ts');
|
||||
expect(entryPoint.packageRoot).toBe('/foo/bar');
|
||||
});
|
||||
});
|
||||
|
||||
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/other/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 entry points for the specified format from each `package.json`', () => {
|
||||
const entryPoints = getEntryPoints(
|
||||
[
|
||||
'/node_modules/@angular/common/package.json',
|
||||
'/node_modules/@angular/common/http/package.json',
|
||||
'/node_modules/@angular/common/http/testing/package.json',
|
||||
'/node_modules/@angular/common/testing/package.json'
|
||||
],
|
||||
'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',
|
||||
'/node_modules/@angular/common/fesm2015/testing.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no matching `package.json` files', () => {
|
||||
const entryPoints = getEntryPoints([], 'fesm2015');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no matching formats', () => {
|
||||
const entryPoints = getEntryPoints(['/node_modules/@angular/common/package.json'], 'fesm3000');
|
||||
expect(entryPoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an entry point even if the typings are not specified', () => {
|
||||
const entryPoints =
|
||||
getEntryPoints(['/node_modules/@angular/no-typings/package.json'], 'fesm2015');
|
||||
expect(entryPoints.length).toEqual(1);
|
||||
expect(entryPoints[0].entryFileName)
|
||||
.toEqual('/node_modules/@angular/no-typings/fesm2015/index.js');
|
||||
expect(entryPoints[0].entryRoot).toEqual('/node_modules/@angular/no-typings/fesm2015');
|
||||
expect(entryPoints[0].dtsEntryRoot).toEqual(entryPoints[0].entryRoot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Marker files', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
describe('writeMarkerFile', () => {
|
||||
it('should write a file containing the version placeholder', () => {
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(false);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
|
||||
writeMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
|
||||
writeMarkerFile('/node_modules/@angular/common/package.json', 'esm5');
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
|
||||
.toBe(true);
|
||||
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
|
||||
expect(
|
||||
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
|
||||
.toEqual('0.0.0-PLACEHOLDER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkMarkerFile', () => {
|
||||
it('should return false if the marker file does not exist', () => {
|
||||
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the marker file exists and contains the correct version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
|
||||
'utf8');
|
||||
expect(checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015')).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw if the marker file exists but contains the wrong version', () => {
|
||||
writeFileSync(
|
||||
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
|
||||
'utf8');
|
||||
expect(() => checkMarkerFile('/node_modules/@angular/common/package.json', 'fesm2015'))
|
||||
.toThrowError(
|
||||
'The ngcc compiler has changed since the last ngcc build.\n' +
|
||||
'Please completely remove `node_modules` and try again.');
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user