angular/packages/compiler-cli/ngcc/test/integration/util.ts
Paul Gschwendtner 8d7f1098d8 refactor: make all imports compatible with ESM/CJS output. (#43431)
As outlined in the previous commit which enabled the `esModuleInterop`
TypeScript compiler option, we need to update all namespace imports
for `typescript` to default imports. This is needed to allow for
TypeScript to be imported at runtime from an ES module.

Similar changes are needed for modules like `semver` where the types incorrectly
suggest named exports that will not exist at runtime when imported from ESM.

This commit refactors all imports to match with the lint rule we have
configured in the previous commit. See the previous commit for more
details on why certain imports have been changed.

A special case are the imports to `@babel/core` and `@babel/types`. For
these a special interop is needed as both default imports, or named
imports break the other module format. e.g default imports would work
well for ESM, but it breaks for CJS. For CJS, the named imports would
only work, but in ESM, only the default export exist. We work around
this for now until the devmode is using ESM as well (which would be
consistent with prodmode and gives us more valuable test results). More
details on the interop can be found in the `babel_core.ts` files (two
interops are needed for both localize/or the compiler-cli).

PR Close #43431
2021-10-01 18:28:45 +00:00

307 lines
11 KiB
TypeScript

/**
* @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 ts from 'typescript';
import {AbsoluteFsPath, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
import {Folder, MockFileSystemPosix} from '../../../src/ngtsc/file_system/testing';
import {loadTestDirectory, loadTsLib, resolveNpmTreeArtifact} from '../../../src/ngtsc/testing';
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.json` 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.json`
* 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.json` 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,
emitDecoratorMetadata: true,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
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,
emitDecoratorMetadata: true,
outDir: './esm2015',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
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,
emitDecoratorMetadata: true,
outDir: './esm5',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
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));
}
export function loadNgccIntegrationTestFiles(): Folder {
const tmpFs = new MockFileSystemPosix(true);
const basePath = '/' as AbsoluteFsPath;
loadTsLib(tmpFs, basePath);
loadTestDirectory(
tmpFs, resolveNpmTreeArtifact('@angular/core-12'),
tmpFs.resolve('/node_modules/@angular/core'));
loadTestDirectory(
tmpFs, resolveNpmTreeArtifact('@angular/common-12'),
tmpFs.resolve('/node_modules/@angular/common'));
loadTestDirectory(
tmpFs, resolveNpmTreeArtifact('typescript'), tmpFs.resolve('/node_modules/typescript'));
loadTestDirectory(tmpFs, resolveNpmTreeArtifact('rxjs'), tmpFs.resolve('/node_modules/rxjs'));
return tmpFs.dump();
}
const stdFiles = loadNgccIntegrationTestFiles();
/**
* 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(stdFiles);
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;
}
}