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;
|
|
}
|
|
}
|