Consider a library that uses a shared constant for host bindings. e.g. ```ts export const BASE_BINDINGS= { '[class.mat-themed]': '_isThemed', } ---- @Directive({ host: {...BASE_BINDINGS, '(click)': '...'} }) export class Dir1 {} @Directive({ host: {...BASE_BINDINGS, '(click)': '...'} }) export class Dir2 {} ``` Previously when these components were shipped as part of the library to NPM, consumers were able to consume `Dir1` and `Dir2`. No errors showed up. Now with Ivy, when ngcc tries to process the library, an error will be thrown. The error is stating that the host bindings should be an object (which they obviously are). This happens because TypeScript transforms the object spread to individual `Object.assign` calls (for compatibility). The partial evaluator used by the `@Directive` annotation handler is unable to process this expression because there is no integrated support for `Object.assign`. In View Engine, this was not a problem because the `metadata.json` files from the library were used to compute the host bindings. Fixes #34659 PR Close #34661
241 lines
8.8 KiB
TypeScript
241 lines
8.8 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, 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: './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;
|
|
}
|
|
}
|