Pete Bacon Darwin 7186f9c016 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
2019-06-25 16:25:24 -07:00

103 lines
3.4 KiB
TypeScript

/**
* @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 {MappingItem, SourceMapConsumer} from 'source-map';
import {NgtscTestEnvironment} from './env';
class TestSourceFile {
private lineStarts: number[];
constructor(public url: string, public contents: string) {
this.lineStarts = this.getLineStarts();
}
getSegment(key: 'generated'|'original', start: MappingItem|any, end: MappingItem|any): string {
const startLine = start[key + 'Line'];
const startCol = start[key + 'Column'];
const endLine = end[key + 'Line'];
const endCol = end[key + 'Column'];
return this.contents.substring(
this.lineStarts[startLine - 1] + startCol, this.lineStarts[endLine - 1] + endCol);
}
getSourceMapFileName(generatedContents: string): string {
const match = /\/\/# sourceMappingURL=(.+)/.exec(generatedContents);
if (!match) {
throw new Error('Generated contents does not contain a sourceMappingURL');
}
return match[1];
}
private getLineStarts(): number[] {
const lineStarts = [0];
let currentPos = 0;
const lines = this.contents.split('\n');
lines.forEach(line => {
currentPos += line.length + 1;
lineStarts.push(currentPos);
});
return lineStarts;
}
}
/**
* A mapping of a segment of generated text to a segment of source text.
*/
export interface SegmentMapping {
/** The generated text in this segment. */
generated: string;
/** The source text in this segment. */
source: string;
/** The URL of the source file for this segment. */
sourceUrl: string;
}
/**
* Process a generated file to extract human understandable segment mappings.
* These mappings are easier to compare in unit tests that the raw SourceMap mappings.
* @param env the environment that holds the source and generated files.
* @param generatedFileName The name of the generated file to process.
* @returns An array of segment mappings for each mapped segment in the given generated file.
*/
export function getMappedSegments(
env: NgtscTestEnvironment, generatedFileName: string): SegmentMapping[] {
const generated = new TestSourceFile(generatedFileName, env.getContents(generatedFileName));
const sourceMapFileName = generated.getSourceMapFileName(generated.contents);
const sources = new Map<string, TestSourceFile>();
const mappings: MappingItem[] = [];
const mapContents = env.getContents(sourceMapFileName);
const sourceMapConsumer = new SourceMapConsumer(JSON.parse(mapContents));
sourceMapConsumer.eachMapping(item => {
if (!sources.has(item.source)) {
sources.set(item.source, new TestSourceFile(item.source, env.getContents(item.source)));
}
mappings.push(item);
});
const segments: SegmentMapping[] = [];
let currentMapping = mappings.shift();
while (currentMapping) {
const nextMapping = mappings.shift();
if (nextMapping) {
const source = sources.get(currentMapping.source) !;
const segment = {
generated: generated.getSegment('generated', currentMapping, nextMapping),
source: source.getSegment('original', currentMapping, nextMapping),
sourceUrl: source.url
};
if (segment.generated !== segment.source) {
segments.push(segment);
}
}
currentMapping = nextMapping;
}
return segments;
}