126 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			126 lines
		
	
	
		
			4.3 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 * as ts from 'typescript'; | ||
|  | 
 | ||
|  | import {FileSystem, getFileSystem} from '../../../src/ngtsc/file_system'; | ||
|  | import {MockFileSystemPosix} from '../../../src/ngtsc/file_system/testing'; | ||
|  | 
 | ||
|  | import {loadStandardTestFiles} from '../../../test/helpers'; | ||
|  | 
 | ||
|  | export type NodeModulesDef = { | ||
|  |   [name: string]: Package | ||
|  | }; | ||
|  | 
 | ||
|  | export type Package = { | ||
|  |   [path: string]: string; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Compile one or more testing packages into the top-level `FileSystem`. | ||
|  |  * | ||
|  |  * Instead of writing ESM5 code by hand, and manually describing the Angular Package Format | ||
|  |  * structure of that code in a mock NPM package, `genNodeModules` allows for the generation of one | ||
|  |  * or more NPM packages from TypeScript source code. Each named NPM package in `def` is | ||
|  |  * independently transpiled with `compileNodeModuleToFs` and written into `node_modules` in the | ||
|  |  * top-level filesystem, ready for use in testing ngcc. | ||
|  |  */ | ||
|  | export function genNodeModules(def: NodeModulesDef): void { | ||
|  |   const fs = getFileSystem(); | ||
|  |   for (const pkgName of Object.keys(def)) { | ||
|  |     compileNodeModuleToFs(fs, pkgName, def[pkgName]); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Takes the TypeScript project defined in the `Package` structure, compiles it to ESM5, and sets it | ||
|  |  * up as a package in `node_modules` in `fs`. | ||
|  |  * | ||
|  |  * TODO(alxhub): over time, expand this to other bundle formats and make it more faithful to the | ||
|  |  * shape of real NPM packages. | ||
|  |  */ | ||
|  | function compileNodeModuleToFs(fs: FileSystem, pkgName: string, pkg: Package): void { | ||
|  |   const compileFs = new MockFileSystemPosix(true); | ||
|  |   compileFs.init(loadStandardTestFiles({fakeCore: false})); | ||
|  | 
 | ||
|  |   const options: ts.CompilerOptions = { | ||
|  |     declaration: true, | ||
|  |     module: ts.ModuleKind.ESNext, | ||
|  |     target: ts.ScriptTarget.ES5, | ||
|  |     lib: [], | ||
|  |   }; | ||
|  | 
 | ||
|  |   const rootNames = Object.keys(pkg); | ||
|  | 
 | ||
|  |   for (const fileName of rootNames) { | ||
|  |     compileFs.writeFile(compileFs.resolve(fileName), pkg[fileName]); | ||
|  |   } | ||
|  | 
 | ||
|  |   const host = new MockCompilerHost(compileFs); | ||
|  |   const program = ts.createProgram({host, rootNames, options}); | ||
|  |   program.emit(); | ||
|  | 
 | ||
|  |   // Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file.
 | ||
|  |   for (const inFileTs of rootNames) { | ||
|  |     const inFileBase = inFileTs.replace(/\.ts$/, ''); | ||
|  |     fs.writeFile( | ||
|  |         fs.resolve(`/node_modules/${pkgName}/${inFileBase}.d.ts`), | ||
|  |         compileFs.readFile(compileFs.resolve(`${inFileBase}.d.ts`))); | ||
|  |     const jsContents = compileFs.readFile(compileFs.resolve(`${inFileBase}.js`)); | ||
|  |     fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.js`), jsContents); | ||
|  |     fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.metadata.json`), '{}'); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Write the package.json
 | ||
|  |   const pkgJson: unknown = { | ||
|  |     name: pkgName, | ||
|  |     version: '0.0.1', | ||
|  |     main: './index.js', | ||
|  |     typings: './index.d.ts', | ||
|  |   }; | ||
|  | 
 | ||
|  |   fs.writeFile( | ||
|  |       fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2)); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * A simple `ts.CompilerHost` that uses a `FileSystem` instead of the real FS. | ||
|  |  * | ||
|  |  * TODO(alxhub): convert this into a first class `FileSystemCompilerHost` and use it as the base for | ||
|  |  * the entire compiler. | ||
|  |  */ | ||
|  | class MockCompilerHost implements ts.CompilerHost { | ||
|  |   constructor(private fs: FileSystem) {} | ||
|  |   getSourceFile( | ||
|  |       fileName: string, languageVersion: ts.ScriptTarget, | ||
|  |       onError?: ((message: string) => void)|undefined, | ||
|  |       shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { | ||
|  |     return ts.createSourceFile( | ||
|  |         fileName, this.fs.readFile(this.fs.resolve(fileName)), languageVersion, true, | ||
|  |         ts.ScriptKind.TS); | ||
|  |   } | ||
|  | 
 | ||
|  |   getDefaultLibFileName(options: ts.CompilerOptions): string { | ||
|  |     return ts.getDefaultLibFileName(options); | ||
|  |   } | ||
|  | 
 | ||
|  |   writeFile(fileName: string, data: string): void { | ||
|  |     this.fs.writeFile(this.fs.resolve(fileName), data); | ||
|  |   } | ||
|  | 
 | ||
|  |   getCurrentDirectory(): string { return this.fs.pwd(); } | ||
|  |   getCanonicalFileName(fileName: string): string { return fileName; } | ||
|  |   useCaseSensitiveFileNames(): boolean { return true; } | ||
|  |   getNewLine(): string { return '\n'; } | ||
|  |   fileExists(fileName: string): boolean { return this.fs.exists(this.fs.resolve(fileName)); } | ||
|  |   readFile(fileName: string): string|undefined { | ||
|  |     const abs = this.fs.resolve(fileName); | ||
|  |     return this.fs.exists(abs) ? this.fs.readFile(abs) : undefined; | ||
|  |   } | ||
|  | } |