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
@ -9,7 +9,7 @@ ts_library(
|
||||
]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler-cli/src/ngtsc/path",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
|
@ -5,26 +5,23 @@
|
||||
* 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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as path from 'path';
|
||||
import {dirname, relative, resolve} from '../../file_system';
|
||||
|
||||
const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
|
||||
|
||||
export function relativePathBetween(from: string, to: string): string|null {
|
||||
let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_JS_EXTENSION, '');
|
||||
let relativePath = relative(dirname(resolve(from)), resolve(to)).replace(TS_DTS_JS_EXTENSION, '');
|
||||
|
||||
if (relative === '') {
|
||||
if (relativePath === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// path.relative() does not include the leading './'.
|
||||
if (!relative.startsWith('.')) {
|
||||
relative = `./${relative}`;
|
||||
if (!relativePath.startsWith('.')) {
|
||||
relativePath = `./${relativePath}`;
|
||||
}
|
||||
|
||||
return relative;
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
export function normalizeSeparators(path: string): string {
|
||||
|
@ -10,7 +10,7 @@ const TS = /\.tsx?$/i;
|
||||
const D_TS = /\.d\.ts$/i;
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {AbsoluteFsPath} from '../../path';
|
||||
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
|
||||
|
||||
export function isDtsPath(filePath: string): boolean {
|
||||
return D_TS.test(filePath);
|
||||
@ -47,6 +47,12 @@ export function getSourceFile(node: ts.Node): ts.SourceFile {
|
||||
return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile();
|
||||
}
|
||||
|
||||
export function getSourceFileOrNull(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile|
|
||||
null {
|
||||
return program.getSourceFile(fileName) || null;
|
||||
}
|
||||
|
||||
|
||||
export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier|null {
|
||||
if (decl.name !== undefined && ts.isIdentifier(decl.name)) {
|
||||
return decl.name;
|
||||
@ -83,7 +89,7 @@ export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions):
|
||||
// See:
|
||||
// https://github.com/Microsoft/TypeScript/blob/3f7357d37f66c842d70d835bc925ec2a873ecfec/src/compiler/sys.ts#L650
|
||||
// Also compiler options might be set via an API which doesn't normalize paths
|
||||
return rootDirs.map(rootDir => AbsoluteFsPath.from(rootDir));
|
||||
return rootDirs.map(rootDir => absoluteFrom(rootDir));
|
||||
}
|
||||
|
||||
export function nodeDebugInfo(node: ts.Node): string {
|
||||
|
@ -10,6 +10,8 @@ ts_library(
|
||||
]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@npm//typescript",
|
||||
|
@ -5,10 +5,10 @@
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
import {makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {makeProgram} from '../../testing';
|
||||
import {VisitListEntryResult, Visitor, visit} from '../src/visitor';
|
||||
|
||||
class TestAstVisitor extends Visitor {
|
||||
@ -43,37 +43,41 @@ function testTransformerFactory(context: ts.TransformationContext): ts.Transform
|
||||
return (file: ts.SourceFile) => visit(file, new TestAstVisitor(), context);
|
||||
}
|
||||
|
||||
describe('AST Visitor', () => {
|
||||
it('should add a statement before class in plain file', () => {
|
||||
const {program, host} =
|
||||
makeProgram([{name: 'main.ts', contents: `class A { static id = 3; }`}]);
|
||||
const sf = program.getSourceFile('main.ts') !;
|
||||
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
|
||||
const main = host.readFile('/main.js');
|
||||
expect(main).toMatch(/^var A_id = 3;/);
|
||||
});
|
||||
runInEachFileSystem(() => {
|
||||
describe('AST Visitor', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
beforeEach(() => _ = absoluteFrom);
|
||||
|
||||
it('should add a statement before class inside function definition', () => {
|
||||
const {program, host} = makeProgram([{
|
||||
name: 'main.ts',
|
||||
contents: `
|
||||
it('should add a statement before class in plain file', () => {
|
||||
const {program, host} =
|
||||
makeProgram([{name: _('/main.ts'), contents: `class A { static id = 3; }`}]);
|
||||
const sf = getSourceFileOrError(program, _('/main.ts'));
|
||||
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
|
||||
const main = host.readFile('/main.js');
|
||||
expect(main).toMatch(/^var A_id = 3;/);
|
||||
});
|
||||
|
||||
it('should add a statement before class inside function definition', () => {
|
||||
const {program, host} = makeProgram([{
|
||||
name: _('/main.ts'),
|
||||
contents: `
|
||||
export function foo() {
|
||||
var x = 3;
|
||||
class A { static id = 2; }
|
||||
return A;
|
||||
}
|
||||
`
|
||||
}]);
|
||||
const sf = program.getSourceFile('main.ts') !;
|
||||
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
|
||||
const main = host.readFile('/main.js');
|
||||
expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/);
|
||||
});
|
||||
}]);
|
||||
const sf = getSourceFileOrError(program, _('/main.ts'));
|
||||
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
|
||||
const main = host.readFile(_('/main.js'));
|
||||
expect(main).toMatch(/var x = 3;\s+var A_id = 2;\s+var A =/);
|
||||
});
|
||||
|
||||
it('handles nested statements', () => {
|
||||
const {program, host} = makeProgram([{
|
||||
name: 'main.ts',
|
||||
contents: `
|
||||
it('handles nested statements', () => {
|
||||
const {program, host} = makeProgram([{
|
||||
name: _('/main.ts'),
|
||||
contents: `
|
||||
export class A {
|
||||
static id = 3;
|
||||
|
||||
@ -84,11 +88,12 @@ describe('AST Visitor', () => {
|
||||
return B;
|
||||
}
|
||||
}`
|
||||
}]);
|
||||
const sf = program.getSourceFile('main.ts') !;
|
||||
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
|
||||
const main = host.readFile('/main.js');
|
||||
expect(main).toMatch(/var A_id = 3;\s+var A = /);
|
||||
expect(main).toMatch(/var B_id = 4;\s+var B = /);
|
||||
}]);
|
||||
const sf = getSourceFileOrError(program, _('/main.ts'));
|
||||
program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]});
|
||||
const main = host.readFile(_('/main.js'));
|
||||
expect(main).toMatch(/var A_id = 3;\s+var A = /);
|
||||
expect(main).toMatch(/var B_id = 4;\s+var B = /);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user