ngcc has a basic integration test infrastructure that downlevels TypeScript code into bundle formats that need to be processed by ngcc. Until now, only ES5 bundles were created with a flat structure, however more complex scenarios require an APF-like layout containing multiple bundle formats. PR Close #33875
		
			
				
	
	
		
			239 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			239 lines
		
	
	
		
			8.7 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 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,
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
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;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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',
 | 
						|
    main: './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): 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();
 | 
						|
  };
 | 
						|
 | 
						|
  // 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: './esm5/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;
 | 
						|
  }
 | 
						|
}
 |