/** * @license * Copyright Google LLC 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 PackageSources = { [path: string]: string; }; /** * Instead of writing packaged code by hand, and manually describing the layout of the package, this * function transpiles the TypeScript sources into a flat file structure using the ES5 format. In * this package layout, all compiled sources are at the root of the package, with .d.ts files next * to the .js files. Each .js also has a corresponding .metadata.js file alongside with it. * * All generated code is written into the `node_modules` in the top-level filesystem, ready for use * in testing ngcc. * * @param pkgName The name of the package to compile. * @param sources The TypeScript sources to compile. */ export function compileIntoFlatEs5Package(pkgName: string, sources: PackageSources): void { compileIntoFlatPackage(pkgName, sources, { target: ts.ScriptTarget.ES5, module: ts.ModuleKind.ESNext, formatProperty: 'esm5', }); } /** * Instead of writing packaged code by hand, and manually describing the layout of the package, * this function transpiles the TypeScript sources into a flat file structure using the ES2015 * format. In this package layout, all compiled sources are at the root of the package, with * `.d.ts` files next to the `.js` files. Each `.js` also has a corresponding `.metadata.js` * file alongside with it. * * All generated code is written into the `node_modules` in the top-level filesystem, ready for use * in testing ngcc. * * @param pkgName The name of the package to compile. * @param sources The TypeScript sources to compile. */ export function compileIntoFlatEs2015Package(pkgName: string, sources: PackageSources): void { compileIntoFlatPackage(pkgName, sources, { target: ts.ScriptTarget.ES2015, module: ts.ModuleKind.ESNext, formatProperty: 'esm2015', }); } export interface FlatLayoutOptions { /** * The script target version to compile into. */ target: ts.ScriptTarget; /** * The module kind to use in the compiled result. */ module: ts.ModuleKind; /** * The name of the property in package.json that refers to the root source file. */ formatProperty: string; } /** * Instead of writing packaged code by hand, and manually describing the layout of the package, this * function transpiles the TypeScript sources into a flat file structure using a single format. In * this package layout, all compiled sources are at the root of the package, with .d.ts files next * to the .js files. Each .js also has a corresponding .metadata.js file alongside with it. * * All generated code is written into the `node_modules` in the top-level filesystem, ready for use * in testing ngcc. * * @param pkgName The name of the package to compile. * @param sources The TypeScript sources to compile. * @param options Allows for configuration of how the sources are compiled. */ function compileIntoFlatPackage( pkgName: string, sources: PackageSources, options: FlatLayoutOptions): void { const fs = getFileSystem(); const {rootNames, compileFs} = setupCompileFs(sources); const emit = (options: ts.CompilerOptions) => { const host = new MockCompilerHost(compileFs); const program = ts.createProgram({host, rootNames, options}); program.emit(); }; emit({declaration: true, module: options.module, target: options.target, lib: []}); // Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file. for (const file of rootNames) { const inFileBase = file.replace(/\.ts$/, ''); const dtsContents = compileFs.readFile(compileFs.resolve(`/${inFileBase}.d.ts`)); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/${inFileBase}.d.ts`), dtsContents); 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', [options.formatProperty]: './index.js', typings: './index.d.ts', }; fs.writeFile( fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2)); } /** * Instead of writing packaged code by hand, and manually describing the layout of the package, this * function transpiles the TypeScript sources into a package layout that of Angular Package Format. * Both esm2015 and esm5 bundles are present in this layout. The .d.ts files reside in the /src * directory and a public .d.ts file is present in the root, re-exporting /src/index.ts. * * Flat modules (fesm2015 and fesm5) and UMD bundles are not generated like they ought to be in APF. * * All generated code is written into the `node_modules` in the top-level filesystem, ready for use * in testing ngcc. */ export function compileIntoApf( pkgName: string, sources: PackageSources, extraCompilerOptions: ts.CompilerOptions = {}): void { const fs = getFileSystem(); const {rootNames, compileFs} = setupCompileFs(sources); const emit = (options: ts.CompilerOptions) => { const host = new MockCompilerHost(compileFs); const program = ts.createProgram({host, rootNames, options: {...extraCompilerOptions, ...options}}); program.emit(); }; // Compile esm2015 into /esm2015 compileFs.ensureDir(compileFs.resolve('esm2015')); emit({ declaration: true, outDir: './esm2015', module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2015, lib: [], }); fs.ensureDir(fs.resolve(`/node_modules/${pkgName}/src`)); fs.ensureDir(fs.resolve(`/node_modules/${pkgName}/esm2015/src`)); for (const file of rootNames) { const inFileBase = file.replace(/\.ts$/, ''); // Copy declaration file into /src tree const dtsContents = compileFs.readFile(compileFs.resolve(`/esm2015/${inFileBase}.d.ts`)); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/src/${inFileBase}.d.ts`), dtsContents); // Copy compiled source file into /esm2015/src tree const jsContents = compileFs.readFile(compileFs.resolve(`/esm2015/${inFileBase}.js`)); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/esm2015/src/${inFileBase}.js`), jsContents); } fs.writeFile( fs.resolve(`/node_modules/${pkgName}/esm2015/index.js`), `export * from './src/index';`); // Compile esm5 into /esm5 compileFs.ensureDir(compileFs.resolve('esm5')); emit({ declaration: false, outDir: './esm5', module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES5, lib: [], }); fs.ensureDir(fs.resolve(`/node_modules/${pkgName}/esm5/src`)); for (const file of rootNames) { const inFileBase = file.replace(/\.ts$/, ''); // Copy compiled source file into esm5/src tree const jsContents = compileFs.readFile(compileFs.resolve(`/esm5/${inFileBase}.js`)); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/esm5/src/${inFileBase}.js`), jsContents); } fs.writeFile( fs.resolve(`/node_modules/${pkgName}/esm5/index.js`), `export * from './src/index';`); // Write a main declaration and metadata file to the root fs.writeFile(fs.resolve(`/node_modules/${pkgName}/index.d.ts`), `export * from './src/index';`); fs.writeFile(fs.resolve(`/node_modules/${pkgName}/index.metadata.json`), '{}'); // Write the package.json const pkgJson: unknown = { name: pkgName, version: '0.0.1', esm5: './esm5/index.js', esm2015: './esm2015/index.js', module: './esm2015/index.js', typings: './index.d.ts', }; fs.writeFile( fs.resolve(`/node_modules/${pkgName}/package.json`), JSON.stringify(pkgJson, null, 2)); } /** * Prepares a mock filesystem that contains all provided source files, which can be used to emit * compiled code into. */ function setupCompileFs(sources: PackageSources): {rootNames: string[], compileFs: FileSystem} { const compileFs = new MockFileSystemPosix(true); compileFs.init(loadStandardTestFiles({fakeCore: false})); const rootNames = Object.keys(sources); for (const fileName of rootNames) { compileFs.writeFile(compileFs.resolve(fileName), sources[fileName]); } return {rootNames, compileFs}; } /** * 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; } }