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
@ -11,6 +11,8 @@ ts_library(
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//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/metadata",
|
||||
"//packages/compiler-cli/src/ngtsc/reflection",
|
||||
|
@ -5,14 +5,14 @@
|
||||
* 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 {ExternalExpr, ExternalReference} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {AliasGenerator, FileToModuleHost, Reference} from '../../imports';
|
||||
import {DtsMetadataReader} from '../../metadata';
|
||||
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||
import {makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {makeProgram} from '../../testing';
|
||||
import {ExportScope} from '../src/api';
|
||||
import {MetadataDtsModuleScopeResolver} from '../src/dependency';
|
||||
|
||||
@ -49,7 +49,7 @@ function makeTestEnv(
|
||||
// Map the modules object to an array of files for `makeProgram`.
|
||||
const files = Object.keys(modules).map(moduleName => {
|
||||
return {
|
||||
name: `node_modules/${moduleName}/index.d.ts`,
|
||||
name: absoluteFrom(`/node_modules/${moduleName}/index.d.ts`),
|
||||
contents: PROLOG + (modules as any)[moduleName],
|
||||
};
|
||||
});
|
||||
@ -79,10 +79,11 @@ function makeTestEnv(
|
||||
};
|
||||
}
|
||||
|
||||
describe('MetadataDtsModuleScopeResolver', () => {
|
||||
it('should produce an accurate scope for a basic NgModule', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'test': `
|
||||
runInEachFileSystem(() => {
|
||||
describe('MetadataDtsModuleScopeResolver', () => {
|
||||
it('should produce an accurate scope for a basic NgModule', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'test': `
|
||||
export declare class Dir {
|
||||
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', ['exportAs'], {'input': 'input2'},
|
||||
{'output': 'output2'}, ['query']>;
|
||||
@ -92,15 +93,15 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<Module, [typeof Dir], never, [typeof Dir]>;
|
||||
}
|
||||
`
|
||||
});
|
||||
const {Dir, Module} = refs;
|
||||
const scope = resolver.resolve(Module) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
});
|
||||
const {Dir, Module} = refs;
|
||||
const scope = resolver.resolve(Module) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
});
|
||||
|
||||
it('should produce an accurate scope when a module is exported', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'test': `
|
||||
it('should produce an accurate scope when a module is exported', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'test': `
|
||||
export declare class Dir {
|
||||
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
|
||||
}
|
||||
@ -113,15 +114,15 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<ModuleB, never, never, [typeof ModuleA]>;
|
||||
}
|
||||
`
|
||||
});
|
||||
const {Dir, ModuleB} = refs;
|
||||
const scope = resolver.resolve(ModuleB) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
});
|
||||
const {Dir, ModuleB} = refs;
|
||||
const scope = resolver.resolve(ModuleB) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
});
|
||||
|
||||
it('should resolve correctly across modules', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'declaration': `
|
||||
it('should resolve correctly across modules', () => {
|
||||
const {resolver, refs} = makeTestEnv({
|
||||
'declaration': `
|
||||
export declare class Dir {
|
||||
static ngDirectiveDef: DirectiveMeta<Dir, '[dir]', never, never, never, never>;
|
||||
}
|
||||
@ -130,26 +131,26 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<ModuleA, [typeof Dir], never, [typeof Dir]>;
|
||||
}
|
||||
`,
|
||||
'exported': `
|
||||
'exported': `
|
||||
import * as d from 'declaration';
|
||||
|
||||
export declare class ModuleB {
|
||||
static ngModuleDef: ModuleMeta<ModuleB, never, never, [typeof d.ModuleA]>;
|
||||
}
|
||||
`
|
||||
});
|
||||
const {Dir, ModuleB} = refs;
|
||||
const scope = resolver.resolve(ModuleB) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
|
||||
// Explicitly verify that the directive has the correct owning module.
|
||||
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
|
||||
});
|
||||
const {Dir, ModuleB} = refs;
|
||||
const scope = resolver.resolve(ModuleB) !;
|
||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
||||
|
||||
// Explicitly verify that the directive has the correct owning module.
|
||||
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
|
||||
});
|
||||
|
||||
it('should write correct aliases for deep dependencies', () => {
|
||||
const {resolver, refs} = makeTestEnv(
|
||||
{
|
||||
'deep': `
|
||||
it('should write correct aliases for deep dependencies', () => {
|
||||
const {resolver, refs} = makeTestEnv(
|
||||
{
|
||||
'deep': `
|
||||
export declare class DeepDir {
|
||||
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
|
||||
}
|
||||
@ -158,7 +159,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
|
||||
}
|
||||
`,
|
||||
'middle': `
|
||||
'middle': `
|
||||
import * as deep from 'deep';
|
||||
|
||||
export declare class MiddleDir {
|
||||
@ -169,7 +170,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<MiddleModule, [typeof MiddleDir], never, [typeof MiddleDir, typeof deep.DeepModule]>;
|
||||
}
|
||||
`,
|
||||
'shallow': `
|
||||
'shallow': `
|
||||
import * as middle from 'middle';
|
||||
|
||||
export declare class ShallowDir {
|
||||
@ -180,26 +181,26 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
|
||||
}
|
||||
`,
|
||||
},
|
||||
new AliasGenerator(testHost));
|
||||
const {ShallowModule} = refs;
|
||||
const scope = resolver.resolve(ShallowModule) !;
|
||||
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
|
||||
expect(getAlias(DeepDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$deep$$DeepDir',
|
||||
},
|
||||
new AliasGenerator(testHost));
|
||||
const {ShallowModule} = refs;
|
||||
const scope = resolver.resolve(ShallowModule) !;
|
||||
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
|
||||
expect(getAlias(DeepDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$deep$$DeepDir',
|
||||
});
|
||||
expect(getAlias(MiddleDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$middle$$MiddleDir',
|
||||
});
|
||||
expect(getAlias(ShallowDir)).toBeNull();
|
||||
});
|
||||
expect(getAlias(MiddleDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$middle$$MiddleDir',
|
||||
});
|
||||
expect(getAlias(ShallowDir)).toBeNull();
|
||||
});
|
||||
|
||||
it('should write correct aliases for bare directives in exports', () => {
|
||||
const {resolver, refs} = makeTestEnv(
|
||||
{
|
||||
'deep': `
|
||||
it('should write correct aliases for bare directives in exports', () => {
|
||||
const {resolver, refs} = makeTestEnv(
|
||||
{
|
||||
'deep': `
|
||||
export declare class DeepDir {
|
||||
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
|
||||
}
|
||||
@ -208,7 +209,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<DeepModule, [typeof DeepDir], never, [typeof DeepDir]>;
|
||||
}
|
||||
`,
|
||||
'middle': `
|
||||
'middle': `
|
||||
import * as deep from 'deep';
|
||||
|
||||
export declare class MiddleDir {
|
||||
@ -219,7 +220,7 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<MiddleModule, [typeof MiddleDir], [typeof deep.DeepModule], [typeof MiddleDir, typeof deep.DeepDir]>;
|
||||
}
|
||||
`,
|
||||
'shallow': `
|
||||
'shallow': `
|
||||
import * as middle from 'middle';
|
||||
|
||||
export declare class ShallowDir {
|
||||
@ -230,27 +231,27 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<ShallowModule, [typeof ShallowDir], never, [typeof ShallowDir, typeof middle.MiddleModule]>;
|
||||
}
|
||||
`,
|
||||
},
|
||||
new AliasGenerator(testHost));
|
||||
const {ShallowModule} = refs;
|
||||
const scope = resolver.resolve(ShallowModule) !;
|
||||
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
|
||||
expect(getAlias(DeepDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$deep$$DeepDir',
|
||||
},
|
||||
new AliasGenerator(testHost));
|
||||
const {ShallowModule} = refs;
|
||||
const scope = resolver.resolve(ShallowModule) !;
|
||||
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
|
||||
expect(getAlias(DeepDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$deep$$DeepDir',
|
||||
});
|
||||
expect(getAlias(MiddleDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$middle$$MiddleDir',
|
||||
});
|
||||
expect(getAlias(ShallowDir)).toBeNull();
|
||||
});
|
||||
expect(getAlias(MiddleDir)).toEqual({
|
||||
moduleName: 'root/shallow',
|
||||
name: 'ɵng$root$middle$$MiddleDir',
|
||||
});
|
||||
expect(getAlias(ShallowDir)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not use an alias if a directive is declared in the same file as the re-exporting module',
|
||||
() => {
|
||||
const {resolver, refs} = makeTestEnv(
|
||||
{
|
||||
'module': `
|
||||
it('should not use an alias if a directive is declared in the same file as the re-exporting module',
|
||||
() => {
|
||||
const {resolver, refs} = makeTestEnv(
|
||||
{
|
||||
'module': `
|
||||
export declare class DeepDir {
|
||||
static ngDirectiveDef: DirectiveMeta<DeepDir, '[deep]', never, never, never, never>;
|
||||
}
|
||||
@ -263,25 +264,26 @@ describe('MetadataDtsModuleScopeResolver', () => {
|
||||
static ngModuleDef: ModuleMeta<DeepExportModule, never, never, [typeof DeepModule]>;
|
||||
}
|
||||
`,
|
||||
},
|
||||
new AliasGenerator(testHost));
|
||||
const {DeepExportModule} = refs;
|
||||
const scope = resolver.resolve(DeepExportModule) !;
|
||||
const [DeepDir] = scopeToRefs(scope);
|
||||
expect(getAlias(DeepDir)).toBeNull();
|
||||
});
|
||||
});
|
||||
},
|
||||
new AliasGenerator(testHost));
|
||||
const {DeepExportModule} = refs;
|
||||
const scope = resolver.resolve(DeepExportModule) !;
|
||||
const [DeepDir] = scopeToRefs(scope);
|
||||
expect(getAlias(DeepDir)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function scopeToRefs(scope: ExportScope): Reference<ClassDeclaration>[] {
|
||||
const directives = scope.exported.directives.map(dir => dir.ref);
|
||||
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
|
||||
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
|
||||
}
|
||||
|
||||
function getAlias(ref: Reference<ClassDeclaration>): ExternalReference|null {
|
||||
if (ref.alias === null) {
|
||||
return null;
|
||||
} else {
|
||||
return (ref.alias as ExternalExpr).value;
|
||||
function scopeToRefs(scope: ExportScope): Reference<ClassDeclaration>[] {
|
||||
const directives = scope.exported.directives.map(dir => dir.ref);
|
||||
const pipes = scope.exported.pipes.map(pipe => pipe.ref);
|
||||
return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !));
|
||||
}
|
||||
}
|
||||
|
||||
function getAlias(ref: Reference<ClassDeclaration>): ExternalReference|null {
|
||||
if (ref.alias === null) {
|
||||
return null;
|
||||
} else {
|
||||
return (ref.alias as ExternalExpr).value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user