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
		
			
				
	
	
		
			173 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			5.9 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
 | |
|  */
 | |
| /// <reference types="node" />
 | |
| import * as fs from 'fs';
 | |
| import * as path from 'path';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| import * as ng from '../index';
 | |
| import {NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';
 | |
| import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../test/helpers';
 | |
| 
 | |
| // TEST_TMPDIR is always set by Bazel.
 | |
| const tmpdir = process.env.TEST_TMPDIR !;
 | |
| 
 | |
| export function makeTempDir(): string {
 | |
|   let dir: string;
 | |
|   while (true) {
 | |
|     const id = (Math.random() * 1000000).toFixed(0);
 | |
|     dir = path.posix.join(tmpdir, `tmp.${id}`);
 | |
|     if (!fs.existsSync(dir)) break;
 | |
|   }
 | |
|   fs.mkdirSync(dir);
 | |
|   return dir;
 | |
| }
 | |
| 
 | |
| export interface TestSupport {
 | |
|   basePath: string;
 | |
|   write(fileName: string, content: string): void;
 | |
|   writeFiles(...mockDirs: {[fileName: string]: string}[]): void;
 | |
|   createCompilerOptions(overrideOptions?: ng.CompilerOptions): ng.CompilerOptions;
 | |
|   shouldExist(fileName: string): void;
 | |
|   shouldNotExist(fileName: string): void;
 | |
| }
 | |
| 
 | |
| function createTestSupportFor(basePath: string) {
 | |
|   // Typescript uses identity comparison on `paths` and other arrays in order to determine
 | |
|   // if program structure can be reused for incremental compilation, so we reuse the default
 | |
|   // values unless overriden, and freeze them so that they can't be accidentaly changed somewhere
 | |
|   // in tests.
 | |
|   const defaultCompilerOptions = {
 | |
|     basePath,
 | |
|     'experimentalDecorators': true,
 | |
|     'skipLibCheck': true,
 | |
|     'strict': true,
 | |
|     'strictPropertyInitialization': false,
 | |
|     'types': Object.freeze<string>([]) as string[],
 | |
|     'outDir': path.resolve(basePath, 'built'),
 | |
|     'rootDir': basePath,
 | |
|     'baseUrl': basePath,
 | |
|     'declaration': true,
 | |
|     'target': ts.ScriptTarget.ES5,
 | |
|     'newLine': ts.NewLineKind.LineFeed,
 | |
|     'module': ts.ModuleKind.ES2015,
 | |
|     'moduleResolution': ts.ModuleResolutionKind.NodeJs,
 | |
|     'lib': Object.freeze([
 | |
|       path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
 | |
|     ]) as string[],
 | |
|     // clang-format off
 | |
|     'paths': Object.freeze({'@angular/*': ['./node_modules/@angular/*']}) as {[index: string]: string[]}
 | |
|     // clang-format on
 | |
|   };
 | |
| 
 | |
| 
 | |
|   return {
 | |
|     // We normalize the basePath into a posix path, so that multiple assertions which compare
 | |
|     // paths don't need to normalize the path separators each time.
 | |
|     basePath: normalizeSeparators(basePath),
 | |
|     write,
 | |
|     writeFiles,
 | |
|     createCompilerOptions,
 | |
|     shouldExist,
 | |
|     shouldNotExist
 | |
|   };
 | |
| 
 | |
|   function ensureDirExists(absolutePathToDir: string) {
 | |
|     if (fs.existsSync(absolutePathToDir)) {
 | |
|       if (!fs.statSync(absolutePathToDir).isDirectory()) {
 | |
|         throw new Error(`'${absolutePathToDir}' exists and is not a directory.`);
 | |
|       }
 | |
|     } else {
 | |
|       const parentDir = path.dirname(absolutePathToDir);
 | |
|       ensureDirExists(parentDir);
 | |
|       fs.mkdirSync(absolutePathToDir);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function write(fileName: string, content: string) {
 | |
|     const absolutePathToFile = path.resolve(basePath, fileName);
 | |
|     ensureDirExists(path.dirname(absolutePathToFile));
 | |
|     fs.writeFileSync(absolutePathToFile, content);
 | |
|   }
 | |
| 
 | |
|   function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
 | |
|     mockDirs.forEach(
 | |
|         (dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); });
 | |
|   }
 | |
| 
 | |
|   function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions {
 | |
|     return {...defaultCompilerOptions, ...overrideOptions};
 | |
|   }
 | |
| 
 | |
|   function shouldExist(fileName: string) {
 | |
|     if (!fs.existsSync(path.resolve(basePath, fileName))) {
 | |
|       throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function shouldNotExist(fileName: string) {
 | |
|     if (fs.existsSync(path.resolve(basePath, fileName))) {
 | |
|       throw new Error(`Did not expect ${fileName} to be emitted (basePath: ${basePath})`);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function setupBazelTo(tmpDirPath: string) {
 | |
|   const nodeModulesPath = path.join(tmpDirPath, 'node_modules');
 | |
|   const angularDirectory = path.join(nodeModulesPath, '@angular');
 | |
| 
 | |
|   fs.mkdirSync(nodeModulesPath);
 | |
|   fs.mkdirSync(angularDirectory);
 | |
| 
 | |
|   getAngularPackagesFromRunfiles().forEach(({pkgPath, name}) => {
 | |
|     fs.symlinkSync(pkgPath, path.join(angularDirectory, name), 'junction');
 | |
|   });
 | |
| 
 | |
|   // Link typescript
 | |
|   const typeScriptSource = resolveNpmTreeArtifact('npm/node_modules/typescript');
 | |
|   const typescriptDest = path.join(nodeModulesPath, 'typescript');
 | |
|   fs.symlinkSync(typeScriptSource, typescriptDest, 'junction');
 | |
| 
 | |
|   // Link "rxjs" if it has been set up as a runfile. "rxjs" is linked optionally because
 | |
|   // not all compiler-cli tests need "rxjs" set up.
 | |
|   try {
 | |
|     const rxjsSource = resolveNpmTreeArtifact('rxjs', 'index.js');
 | |
|     const rxjsDest = path.join(nodeModulesPath, 'rxjs');
 | |
|     fs.symlinkSync(rxjsSource, rxjsDest, 'junction');
 | |
|   } catch (e) {
 | |
|     if (e.code !== 'MODULE_NOT_FOUND') throw e;
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function setup(): TestSupport {
 | |
|   // // `TestSupport` provides its own file-system abstraction so we just use
 | |
|   // // the native `NodeJSFileSystem` under the hood.
 | |
|   setFileSystem(new NodeJSFileSystem());
 | |
|   const tmpDirPath = makeTempDir();
 | |
|   setupBazelTo(tmpDirPath);
 | |
|   return createTestSupportFor(tmpDirPath);
 | |
| }
 | |
| 
 | |
| export function expectNoDiagnostics(options: ng.CompilerOptions, diags: ng.Diagnostics) {
 | |
|   const errorDiags = diags.filter(d => d.category !== ts.DiagnosticCategory.Message);
 | |
|   if (errorDiags.length) {
 | |
|     throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(errorDiags)}`);
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function expectNoDiagnosticsInProgram(options: ng.CompilerOptions, p: ng.Program) {
 | |
|   expectNoDiagnostics(options, [
 | |
|     ...p.getNgStructuralDiagnostics(), ...p.getTsSemanticDiagnostics(),
 | |
|     ...p.getNgSemanticDiagnostics()
 | |
|   ]);
 | |
| }
 | |
| 
 | |
| export function normalizeSeparators(path: string): string {
 | |
|   return path.replace(/\\/g, '/');
 | |
| }
 |