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
@ -10,7 +10,7 @@ ts_library(
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/path",
|
||||
"//packages/compiler-cli/src/ngtsc/file_system",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
"//packages/compiler-cli/src/ngtsc/util",
|
||||
"@npm//@types/node",
|
||||
|
@ -5,19 +5,15 @@
|
||||
* 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 {Expression, ExternalExpr, WrappedNodeExpr} from '@angular/compiler';
|
||||
import {ExternalReference} from '@angular/compiler/src/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {LogicalFileSystem, LogicalProjectPath} from '../../path';
|
||||
import {LogicalFileSystem, LogicalProjectPath, absoluteFrom} from '../../file_system';
|
||||
import {ReflectionHost} from '../../reflection';
|
||||
import {getSourceFile, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
|
||||
|
||||
import {getSourceFile, getSourceFileOrNull, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
|
||||
import {findExportedNameOfNode} from './find_export';
|
||||
import {ImportMode, Reference} from './references';
|
||||
|
||||
|
||||
/**
|
||||
* A host which supports an operation to convert a file name into a module name.
|
||||
*
|
||||
@ -170,8 +166,9 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entryPointFile = this.program.getSourceFile(resolvedModule.resolvedFileName);
|
||||
if (entryPointFile === undefined) {
|
||||
const entryPointFile =
|
||||
getSourceFileOrNull(this.program, absoluteFrom(resolvedModule.resolvedFileName));
|
||||
if (entryPointFile === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,9 @@
|
||||
* 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 {resolveModuleName} from '../../util/src/typescript';
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
import {getSourceFileOrNull, resolveModuleName} from '../../util/src/typescript';
|
||||
import {Reference} from './references';
|
||||
|
||||
export interface ReferenceResolver {
|
||||
@ -33,6 +32,6 @@ export class ModuleResolver {
|
||||
if (resolved === undefined) {
|
||||
return null;
|
||||
}
|
||||
return this.program.getSourceFile(resolved.resolvedFileName) || null;
|
||||
return getSourceFileOrNull(this.program, absoluteFrom(resolved.resolvedFileName));
|
||||
}
|
||||
}
|
||||
|
@ -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/imports",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"@npm//typescript",
|
||||
|
@ -5,86 +5,101 @@
|
||||
* 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 {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {getDeclaration, makeProgram} from '../../testing';
|
||||
import {DefaultImportTracker} from '../src/default';
|
||||
|
||||
describe('DefaultImportTracker', () => {
|
||||
it('should prevent a default import from being elided if used', () => {
|
||||
const {program, host} = makeProgram(
|
||||
[
|
||||
{name: 'dep.ts', contents: `export default class Foo {}`},
|
||||
{name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
|
||||
runInEachFileSystem(() => {
|
||||
describe('DefaultImportTracker', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
beforeEach(() => _ = absoluteFrom);
|
||||
|
||||
// This control file is identical to the test file, but will not have its import marked
|
||||
// for preservation. It exists to verify that it is in fact the action of
|
||||
// DefaultImportTracker and not some other artifact of the test setup which causes the
|
||||
// import to be preserved. It will also verify that DefaultImportTracker does not preserve
|
||||
// imports which are not marked for preservation.
|
||||
{name: 'ctrl.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
|
||||
],
|
||||
{
|
||||
module: ts.ModuleKind.ES2015,
|
||||
});
|
||||
const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause);
|
||||
const fooId = fooClause.name !;
|
||||
const fooDecl = fooClause.parent;
|
||||
it('should prevent a default import from being elided if used', () => {
|
||||
const {program, host} = makeProgram(
|
||||
[
|
||||
{name: _('/dep.ts'), contents: `export default class Foo {}`},
|
||||
{
|
||||
name: _('/test.ts'),
|
||||
contents: `import Foo from './dep'; export function test(f: Foo) {}`
|
||||
},
|
||||
|
||||
const tracker = new DefaultImportTracker();
|
||||
tracker.recordImportedIdentifier(fooId, fooDecl);
|
||||
tracker.recordUsedIdentifier(fooId);
|
||||
program.emit(undefined, undefined, undefined, undefined, {
|
||||
before: [tracker.importPreservingTransformer()],
|
||||
// This control file is identical to the test file, but will not have its import marked
|
||||
// for preservation. It exists to verify that it is in fact the action of
|
||||
// DefaultImportTracker and not some other artifact of the test setup which causes the
|
||||
// import to be preserved. It will also verify that DefaultImportTracker does not
|
||||
// preserve imports which are not marked for preservation.
|
||||
{
|
||||
name: _('/ctrl.ts'),
|
||||
contents: `import Foo from './dep'; export function test(f: Foo) {}`
|
||||
},
|
||||
],
|
||||
{
|
||||
module: ts.ModuleKind.ES2015,
|
||||
});
|
||||
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
|
||||
const fooId = fooClause.name !;
|
||||
const fooDecl = fooClause.parent;
|
||||
|
||||
const tracker = new DefaultImportTracker();
|
||||
tracker.recordImportedIdentifier(fooId, fooDecl);
|
||||
tracker.recordUsedIdentifier(fooId);
|
||||
program.emit(undefined, undefined, undefined, undefined, {
|
||||
before: [tracker.importPreservingTransformer()],
|
||||
});
|
||||
const testContents = host.readFile('/test.js') !;
|
||||
expect(testContents).toContain(`import Foo from './dep';`);
|
||||
|
||||
// The control should have the import elided.
|
||||
const ctrlContents = host.readFile('/ctrl.js');
|
||||
expect(ctrlContents).not.toContain(`import Foo from './dep';`);
|
||||
});
|
||||
const testContents = host.readFile('/test.js') !;
|
||||
expect(testContents).toContain(`import Foo from './dep';`);
|
||||
|
||||
// The control should have the import elided.
|
||||
const ctrlContents = host.readFile('/ctrl.js');
|
||||
expect(ctrlContents).not.toContain(`import Foo from './dep';`);
|
||||
it('should transpile imports correctly into commonjs', () => {
|
||||
const {program, host} = makeProgram(
|
||||
[
|
||||
{name: _('/dep.ts'), contents: `export default class Foo {}`},
|
||||
{
|
||||
name: _('/test.ts'),
|
||||
contents: `import Foo from './dep'; export function test(f: Foo) {}`
|
||||
},
|
||||
],
|
||||
{
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
});
|
||||
const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause);
|
||||
const fooId = ts.updateIdentifier(fooClause.name !);
|
||||
const fooDecl = fooClause.parent;
|
||||
|
||||
const tracker = new DefaultImportTracker();
|
||||
tracker.recordImportedIdentifier(fooId, fooDecl);
|
||||
tracker.recordUsedIdentifier(fooId);
|
||||
program.emit(undefined, undefined, undefined, undefined, {
|
||||
before: [
|
||||
addReferenceTransformer(fooId),
|
||||
tracker.importPreservingTransformer(),
|
||||
],
|
||||
});
|
||||
const testContents = host.readFile('/test.js') !;
|
||||
expect(testContents).toContain(`var dep_1 = require("./dep");`);
|
||||
expect(testContents).toContain(`var ref = dep_1["default"];`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should transpile imports correctly into commonjs', () => {
|
||||
const {program, host} = makeProgram(
|
||||
[
|
||||
{name: 'dep.ts', contents: `export default class Foo {}`},
|
||||
{name: 'test.ts', contents: `import Foo from './dep'; export function test(f: Foo) {}`},
|
||||
],
|
||||
{
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
});
|
||||
const fooClause = getDeclaration(program, 'test.ts', 'Foo', ts.isImportClause);
|
||||
const fooId = ts.updateIdentifier(fooClause.name !);
|
||||
const fooDecl = fooClause.parent;
|
||||
|
||||
const tracker = new DefaultImportTracker();
|
||||
tracker.recordImportedIdentifier(fooId, fooDecl);
|
||||
tracker.recordUsedIdentifier(fooId);
|
||||
program.emit(undefined, undefined, undefined, undefined, {
|
||||
before: [
|
||||
addReferenceTransformer(fooId),
|
||||
tracker.importPreservingTransformer(),
|
||||
],
|
||||
});
|
||||
const testContents = host.readFile('/test.js') !;
|
||||
expect(testContents).toContain(`var dep_1 = require("./dep");`);
|
||||
expect(testContents).toContain(`var ref = dep_1["default"];`);
|
||||
});
|
||||
});
|
||||
|
||||
function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context: ts.TransformationContext) => {
|
||||
return (sf: ts.SourceFile) => {
|
||||
if (id.getSourceFile().fileName === sf.fileName) {
|
||||
return ts.updateSourceFileNode(sf, [
|
||||
...sf.statements, ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
|
||||
ts.createVariableDeclaration('ref', undefined, id),
|
||||
]))
|
||||
]);
|
||||
}
|
||||
return sf;
|
||||
function addReferenceTransformer(id: ts.Identifier): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context: ts.TransformationContext) => {
|
||||
return (sf: ts.SourceFile) => {
|
||||
if (id.getSourceFile().fileName === sf.fileName) {
|
||||
return ts.updateSourceFileNode(sf, [
|
||||
...sf.statements,
|
||||
ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
|
||||
ts.createVariableDeclaration('ref', undefined, id),
|
||||
]))
|
||||
]);
|
||||
}
|
||||
return sf;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user