refactor(ivy): implement a virtual file-system layer in ngtsc + ngcc (#30921)
To improve cross platform support, all file access (and path manipulation) is now done through a well known interface (`FileSystem`). For testing a number of `MockFileSystem` implementations are provided. These provide an in-memory file-system which emulates operating systems like OS/X, Unix and Windows. The current file system is always available via the static method, `FileSystem.getFileSystem()`. This is also used by a number of static methods on `AbsoluteFsPath` and `PathSegment`, to avoid having to pass `FileSystem` objects around all the time. The result of this is that one must be careful to ensure that the file-system has been initialized before using any of these static methods. To prevent this happening accidentally the current file system always starts out as an instance of `InvalidFileSystem`, which will throw an error if any of its methods are called. You can set the current file-system by calling `FileSystem.setFileSystem()`. During testing you can call the helper function `initMockFileSystem(os)` which takes a string name of the OS to emulate, and will also monkey-patch aspects of the TypeScript library to ensure that TS is also using the current file-system. Finally there is the `NgtscCompilerHost` to be used for any TypeScript compilation, which uses a given file-system. All tests that interact with the file-system should be tested against each of the mock file-systems. A series of helpers have been provided to support such tests: * `runInEachFileSystem()` - wrap your tests in this helper to run all the wrapped tests in each of the mock file-systems. * `addTestFilesToFileSystem()` - use this to add files and their contents to the mock file system for testing. * `loadTestFilesFromDisk()` - use this to load a mirror image of files on disk into the in-memory mock file-system. * `loadFakeCore()` - use this to load a fake version of `@angular/core` into the mock file-system. All ngcc and ngtsc source and tests now use this virtual file-system setup. PR Close #30921
This commit is contained in:

committed by
Kara Erickson

parent
1e7e065423
commit
7186f9c016
@ -6,163 +6,186 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||
import {FileSystem} from '../../src/file_system/file_system';
|
||||
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
|
||||
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||
import {loadTestFiles} from '../../../test/helpers';
|
||||
import {getEntryPointInfo} from '../../src/packages/entry_point';
|
||||
import {MockFileSystem} from '../helpers/mock_file_system';
|
||||
import {MockLogger} from '../helpers/mock_logger';
|
||||
|
||||
const _ = AbsoluteFsPath.from;
|
||||
runInEachFileSystem(() => {
|
||||
describe('getEntryPointInfo()', () => {
|
||||
let SOME_PACKAGE: AbsoluteFsPath;
|
||||
let _: typeof absoluteFrom;
|
||||
let fs: FileSystem;
|
||||
|
||||
describe('getEntryPointInfo()', () => {
|
||||
const SOME_PACKAGE = _('/some_package');
|
||||
beforeEach(() => {
|
||||
setupMockFileSystem();
|
||||
SOME_PACKAGE = absoluteFrom('/some_package');
|
||||
_ = absoluteFrom;
|
||||
fs = getFileSystem();
|
||||
});
|
||||
|
||||
it('should return an object containing absolute paths to the formats of the specified entry-point',
|
||||
() => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some-package/valid_entry_point',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/valid_entry_point'),
|
||||
typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'),
|
||||
compiledByAngular: true,
|
||||
it('should return an object containing absolute paths to the formats of the specified entry-point',
|
||||
() => {
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/valid_entry_point'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some-package/valid_entry_point',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/valid_entry_point'),
|
||||
typings: _(`/some_package/valid_entry_point/valid_entry_point.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/valid_entry_point'),
|
||||
compiledByAngular: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there is no package.json at the entry-point path', () => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json'));
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
it('should return null if there is no package.json at the entry-point path', () => {
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_package_json'));
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if there is no typings or types field in the package.json', () => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint =
|
||||
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings'));
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
it('should return null if there is no typings or types field in the package.json', () => {
|
||||
const entryPoint =
|
||||
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_typings'));
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
|
||||
it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file',
|
||||
() => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some-package/missing_metadata',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/missing_metadata'),
|
||||
typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/missing_metadata'),
|
||||
compiledByAngular: false,
|
||||
it('should return an object with `compiledByAngular` set to false if there is no metadata.json file next to the typing file',
|
||||
() => {
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/missing_metadata'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some-package/missing_metadata',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/missing_metadata'),
|
||||
typings: _(`/some_package/missing_metadata/missing_metadata.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/missing_metadata'),
|
||||
compiledByAngular: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should work if the typings field is named `types', () => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some-package/types_rather_than_typings',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/types_rather_than_typings'),
|
||||
typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'),
|
||||
compiledByAngular: true,
|
||||
it('should work if the typings field is named `types', () => {
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/types_rather_than_typings'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some-package/types_rather_than_typings',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/types_rather_than_typings'),
|
||||
typings: _(`/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/types_rather_than_typings'),
|
||||
compiledByAngular: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with Angular Material style package.json', () => {
|
||||
const entryPoint =
|
||||
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some_package/material_style',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/material_style'),
|
||||
typings: _(`/some_package/material_style/material_style.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/material_style'),
|
||||
compiledByAngular: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if the package.json is not valid JSON', () => {
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols'));
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with Angular Material style package.json', () => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint =
|
||||
getEntryPointInfo(fs, new MockLogger(), SOME_PACKAGE, _('/some_package/material_style'));
|
||||
expect(entryPoint).toEqual({
|
||||
name: 'some_package/material_style',
|
||||
package: SOME_PACKAGE,
|
||||
path: _('/some_package/material_style'),
|
||||
typings: _(`/some_package/material_style/material_style.d.ts`),
|
||||
packageJson: loadPackageJson(fs, '/some_package/material_style'),
|
||||
compiledByAngular: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if the package.json is not valid JSON', () => {
|
||||
const fs = createMockFileSystem();
|
||||
const entryPoint = getEntryPointInfo(
|
||||
fs, new MockLogger(), SOME_PACKAGE, _('/some_package/unexpected_symbols'));
|
||||
expect(entryPoint).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockFileSystem() {
|
||||
return new MockFileSystem({
|
||||
'/some_package': {
|
||||
'valid_entry_point': {
|
||||
'package.json': createPackageJson('valid_entry_point'),
|
||||
'valid_entry_point.metadata.json': 'some meta data',
|
||||
function setupMockFileSystem(): void {
|
||||
const _ = absoluteFrom;
|
||||
loadTestFiles([
|
||||
{
|
||||
name: _('/some_package/valid_entry_point/package.json'),
|
||||
contents: createPackageJson('valid_entry_point')
|
||||
},
|
||||
'missing_package_json': {
|
||||
// no package.json!
|
||||
'missing_package_json.metadata.json': 'some meta data',
|
||||
{
|
||||
name: _('/some_package/valid_entry_point/valid_entry_point.metadata.json'),
|
||||
contents: 'some meta data'
|
||||
},
|
||||
'missing_typings': {
|
||||
'package.json': createPackageJson('missing_typings', {excludes: ['typings']}),
|
||||
'missing_typings.metadata.json': 'some meta data',
|
||||
// no package.json!
|
||||
{
|
||||
name: _('/some_package/missing_package_json/missing_package_json.metadata.json'),
|
||||
contents: 'some meta data'
|
||||
},
|
||||
'types_rather_than_typings': {
|
||||
'package.json': createPackageJson('types_rather_than_typings', {}, 'types'),
|
||||
'types_rather_than_typings.metadata.json': 'some meta data',
|
||||
{
|
||||
name: _('/some_package/missing_typings/package.json'),
|
||||
contents: createPackageJson('missing_typings', {excludes: ['typings']})
|
||||
},
|
||||
'missing_esm2015': {
|
||||
'package.json': createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']}),
|
||||
'missing_esm2015.metadata.json': 'some meta data',
|
||||
{
|
||||
name: _('/some_package/missing_typings/missing_typings.metadata.json'),
|
||||
contents: 'some meta data'
|
||||
},
|
||||
'missing_metadata': {
|
||||
'package.json': createPackageJson('missing_metadata'),
|
||||
// no metadata.json!
|
||||
{
|
||||
name: _('/some_package/types_rather_than_typings/package.json'),
|
||||
contents: createPackageJson('types_rather_than_typings', {}, 'types')
|
||||
},
|
||||
'material_style': {
|
||||
'package.json': `{
|
||||
{
|
||||
name: _('/some_package/types_rather_than_typings/types_rather_than_typings.metadata.json'),
|
||||
contents: 'some meta data'
|
||||
},
|
||||
{
|
||||
name: _('/some_package/missing_esm2015/package.json'),
|
||||
contents: createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']})
|
||||
},
|
||||
{
|
||||
name: _('/some_package/missing_esm2015/missing_esm2015.metadata.json'),
|
||||
contents: 'some meta data'
|
||||
},
|
||||
// no metadata.json!
|
||||
{
|
||||
name: _('/some_package/missing_metadata/package.json'),
|
||||
contents: createPackageJson('missing_metadata')
|
||||
},
|
||||
{
|
||||
name: _('/some_package/material_style/package.json'),
|
||||
contents: `{
|
||||
"name": "some_package/material_style",
|
||||
"typings": "./material_style.d.ts",
|
||||
"main": "./bundles/material_style.umd.js",
|
||||
"module": "./esm5/material_style.es5.js",
|
||||
"es2015": "./esm2015/material_style.js"
|
||||
}`,
|
||||
'material_style.metadata.json': 'some meta data',
|
||||
}`
|
||||
},
|
||||
'unexpected_symbols': {
|
||||
// package.json might not be a valid JSON
|
||||
// for example, @schematics/angular contains a package.json blueprint
|
||||
// with unexpected symbols
|
||||
'package.json':
|
||||
'{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}',
|
||||
{
|
||||
name: _('/some_package/material_style/material_style.metadata.json'),
|
||||
contents: 'some meta data'
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createPackageJson(
|
||||
packageName: string, {excludes}: {excludes?: string[]} = {},
|
||||
typingsProp: string = 'typings'): string {
|
||||
const packageJson: any = {
|
||||
name: `some-package/${packageName}`,
|
||||
[typingsProp]: `./${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 (excludes) {
|
||||
excludes.forEach(exclude => delete packageJson[exclude]);
|
||||
// package.json might not be a valid JSON
|
||||
// for example, @schematics/angular contains a package.json blueprint
|
||||
// with unexpected symbols
|
||||
{
|
||||
name: _('/some_package/unexpected_symbols/package.json'),
|
||||
contents: '{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}'
|
||||
},
|
||||
]);
|
||||
}
|
||||
return JSON.stringify(packageJson);
|
||||
}
|
||||
|
||||
function createPackageJson(
|
||||
packageName: string, {excludes}: {excludes?: string[]} = {},
|
||||
typingsProp: string = 'typings'): string {
|
||||
const packageJson: any = {
|
||||
name: `some-package/${packageName}`,
|
||||
[typingsProp]: `./${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 (excludes) {
|
||||
excludes.forEach(exclude => delete packageJson[exclude]);
|
||||
}
|
||||
return JSON.stringify(packageJson);
|
||||
}
|
||||
});
|
||||
|
||||
export function loadPackageJson(fs: FileSystem, packagePath: string) {
|
||||
return JSON.parse(fs.readFile(_(packagePath + '/package.json')));
|
||||
return JSON.parse(fs.readFile(fs.resolve(packagePath + '/package.json')));
|
||||
}
|
||||
|
Reference in New Issue
Block a user