/**
* @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 {readFileSync} from 'fs';
import * as os from 'os';
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, join} from '../../../src/ngtsc/file_system';
import {Folder, MockFileSystem, runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
import {getLockFilePath} from '../../src/locking/lock_file';
import {mainNgcc} from '../../src/main';
import {clearTsConfigCache} from '../../src/ngcc_options';
import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker';
import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
import {EntryPointManifestFile} from '../../src/packages/entry_point_manifest';
import {Transformer} from '../../src/packages/transformer';
import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater';
import {compileIntoApf, compileIntoFlatEs2015Package, compileIntoFlatEs5Package} from './util';
const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
runInEachFileSystem(() => {
describe('ngcc main()', () => {
let _: typeof absoluteFrom;
let fs: FileSystem;
let pkgJsonUpdater: PackageJsonUpdater;
const STANDARD_MARKERS = {
main: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
esm2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
};
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
pkgJsonUpdater = new DirectPackageJsonUpdater(fs);
initMockFileSystem(fs, testFiles);
// Force single-process execution in unit tests by mocking available CPUs to 1.
spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]);
});
afterEach(() => {
clearTsConfigCache();
});
/**
* Sets up the esm5 format in the Angular core package. By default, package output
* no longer contains esm5 output, so we process the fesm2015 file into ES5 and
* link it as if its the ESM5 output.
*/
function setupAngularCoreEsm5() {
const pkgPath = _('/node_modules/@angular/core');
const pkgJsonPath = fs.join(pkgPath, 'package.json');
const pkgJson = JSON.parse(fs.readFile(pkgJsonPath));
fs.ensureDir(fs.join(pkgPath, 'fesm5'));
fs.writeFile(
fs.join(pkgPath, 'fesm5/core.js'),
readFileSync(require.resolve('../fesm5_angular_core.js'), 'utf8'));
pkgJson.esm5 = './fesm5/core.js';
pkgJson.fesm5 = './fesm5/core.js';
fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
}
it('should run ngcc without errors for esm2015', () => {
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']}))
.not.toThrow();
});
it('should run ngcc without errors for esm5', () => {
setupAngularCoreEsm5();
expect(() => mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm5'],
targetEntryPointPath: '@angular/core',
logger: new MockLogger(),
}))
.not.toThrow();
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeDefined();
});
it('should run ngcc without errors when "main" property is not present', () => {
mainNgcc({
basePath: '/dist',
propertiesToConsider: ['main', 'es2015'],
logger: new MockLogger(),
});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should throw, if some of the entry-points are unprocessable', () => {
const createEntryPoint = (name: string, prop: EntryPointJsonProperty): TestFile[] => {
return [
{
name: _(`/dist/${name}/package.json`),
contents: `{"name": "${name}", "typings": "./index.d.ts", "${prop}": "./index.js"}`,
},
{name: _(`/dist/${name}/index.js`), contents: 'var DUMMY_DATA = true;'},
{name: _(`/dist/${name}/index.d.ts`), contents: 'export type DummyData = boolean;'},
{name: _(`/dist/${name}/index.metadata.json`), contents: 'DUMMY DATA'},
];
};
loadTestFiles([
...createEntryPoint('processable-1', 'es2015'),
...createEntryPoint('unprocessable-2', 'main'),
...createEntryPoint('unprocessable-3', 'main'),
]);
expect(() => mainNgcc({
basePath: '/dist',
propertiesToConsider: ['es2015', 'fesm5', 'module'],
logger: new MockLogger(),
}))
.toThrowError(
'Unable to process any formats for the following entry-points (tried es2015, fesm5, module): \n' +
` - ${_('/dist/unprocessable-2')}\n` +
` - ${_('/dist/unprocessable-3')}`);
});
it('should throw, if an error happens during processing', () => {
spyOn(Transformer.prototype, 'transform').and.throwError('Test error.');
expect(() => mainNgcc({
basePath: '/dist',
targetEntryPointPath: 'local-package',
propertiesToConsider: ['main', 'es2015'],
logger: new MockLogger(),
}))
.toThrowError(`Test error.`);
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toBeUndefined();
});
it('should generate correct metadata for decorated getter/setter properties', () => {
setupAngularCoreEsm5();
compileIntoFlatEs5Package('test-package', {
'/index.ts': `
import {Directive, Input, NgModule} from '@angular/core';
@Directive({selector: '[foo]'})
export class FooDirective {
@Input() get bar() { return 'bar'; }
set bar(value: string) {}
}
@NgModule({
declarations: [FooDirective],
})
export class FooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm5'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)).replace(/\s+/g, ' ');
expect(jsContents)
.toContain(
'/*@__PURE__*/ (function () { ɵngcc0.ɵsetClassMetadata(FooDirective, ' +
'[{ type: Directive, args: [{ selector: \'[foo]\' }] }], ' +
'function () { return []; }, ' +
'{ bar: [{ type: Input }] }); })();');
});
['esm5', 'esm2015'].forEach(target => {
it(`should be able to process spread operator inside objects for ${
target} format (imported helpers)`,
() => {
setupAngularCoreEsm5();
compileIntoApf(
'test-package', {
'/index.ts': `
import {Directive, Input, NgModule} from '@angular/core';
const a = { '[class.a]': 'true' };
const b = { '[class.b]': 'true' };
@Directive({
selector: '[foo]',
host: {...a, ...b, '[class.c]': 'false'}
})
export class FooDirective {}
@NgModule({
declarations: [FooDirective],
})
export class FooModule {}
`,
},
{importHelpers: true, noEmitHelpers: true});
fs.writeFile(
_('/node_modules/tslib/index.d.ts'),
`export declare function __assign(...args: object[]): object;`);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: [target],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/${target}/src/index.js`))
.replace(/\s+/g, ' ');
expect(jsContents).toContain('ngcc0.ɵɵclassProp("a", true)("b", true)("c", false)');
});
it(`should be able to process emitted spread operator inside objects for ${
target} format (emitted helpers)`,
() => {
setupAngularCoreEsm5();
compileIntoApf(
'test-package', {
'/index.ts': `
import {Directive, Input, NgModule} from '@angular/core';
const a = { '[class.a]': 'true' };
const b = { '[class.b]': 'true' };
@Directive({
selector: '[foo]',
host: {...a, ...b, '[class.c]': 'false'}
})
export class FooDirective {}
@NgModule({
declarations: [FooDirective],
})
export class FooModule {}
`,
},
{importHelpers: false, noEmitHelpers: false});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: [target],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/${target}/src/index.js`))
.replace(/\s+/g, ' ');
expect(jsContents).toContain('ngcc0.ɵɵclassProp("a", true)("b", true)("c", false)');
});
});
it('should not add `const` in ES5 generated code', () => {
setupAngularCoreEsm5();
compileIntoFlatEs5Package('test-package', {
'/index.ts': `
import {Directive, Input, NgModule} from '@angular/core';
@Directive({
selector: '[foo]',
host: {bar: ''},
})
export class FooDirective {
}
@NgModule({
declarations: [FooDirective],
})
export class FooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm5'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents).not.toMatch(/\bconst \w+\s*=/);
});
it('should be able to reflect into external libraries', () => {
compileIntoApf('lib', {
'/index.ts': `
export * from './constants';
export * from './module';
`,
'/constants.ts': `
export const selectorA = '[selector-a]';
export class Selectors {
static readonly B = '[selector-b]';
}
`,
'/module.ts': `
import {NgModule, ModuleWithProviders} from '@angular/core';
@NgModule()
export class MyOtherModule {}
export class MyModule {
static forRoot(): ModuleWithProviders {
return {ngModule: MyOtherModule};
}
}
`
});
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {Directive, Input, NgModule} from '@angular/core';
import * as lib from 'lib';
@Directive({
selector: lib.selectorA,
})
export class DirectiveA {
}
@Directive({
selector: lib.Selectors.B,
})
export class DirectiveB {
}
@NgModule({
imports: [lib.MyModule.forRoot()],
declarations: [DirectiveA, DirectiveB],
})
export class FooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents).toContain('"selector-a"');
expect(jsContents).toContain('"selector-b"');
expect(jsContents).toContain('imports: [ɵngcc1.MyOtherModule]');
});
it('should be able to resolve enum values', () => {
compileIntoApf('test-package', {
'/index.ts': `
import {Component, NgModule} from '@angular/core';
export enum StringEnum {
ValueA = "a",
ValueB = "b",
}
export enum NumericEnum {
Value3 = 3,
Value4,
}
@Component({
template: \`\${StringEnum.ValueA} - \${StringEnum.ValueB} - \${NumericEnum.Value3} - \${NumericEnum.Value4}\`,
})
export class FooCmp {}
@NgModule({
declarations: [FooCmp],
})
export class FooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015', 'esm5'],
});
const es2015Contents = fs.readFile(_(`/node_modules/test-package/esm2015/src/index.js`));
expect(es2015Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
const es5Contents = fs.readFile(_(`/node_modules/test-package/esm5/src/index.js`));
expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
});
it('should not crash when scanning for ModuleWithProviders needs to evaluate code from an external package',
() => {
// Regression test for https://github.com/angular/angular/issues/37508
// During `ModuleWithProviders` analysis, return statements in methods are evaluated using
// the partial evaluator to identify whether they correspond with a `ModuleWithProviders`
// function. If an arbitrary method has a return statement that calls into an external
// module which doesn't have declaration files, ngcc would attempt to reflect on said
// module using the reflection host of the entry-point. This would crash in the case where
// e.g. the entry-point is UMD and the external module would be CommonJS, as the UMD
// reflection host would throw because it is unable to deal with CommonJS.
// Setup a non-TS package with CommonJS module format
loadTestFiles([
{
name: _(`/node_modules/identity/package.json`),
contents: `{"name": "identity", "main": "./index.js"}`,
},
{
name: _(`/node_modules/identity/index.js`),
contents: `
function identity(x) { return x; };
exports.identity = identity;
module.exports = identity;
`,
},
]);
// Setup an Angular entry-point with UMD module format that references an export of the
// CommonJS package.
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: '{"name": "test-package", "main": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package/index.js'),
contents: `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('identity')) :
typeof define === 'function' && define.amd ? define('test', ['exports', 'identity'], factory) :
(factory(global.test, global.identity));
}(this, (function (exports, identity) { 'use strict';
function Foo(x) {
// The below statement is analyzed for 'ModuleWithProviders', so is evaluated
// by ngcc. The reference into the non-TS CommonJS package used to crash ngcc.
return identity.identity(x);
}
exports.Foo = Foo;
})));
`
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents: 'export declare class Foo { static doSomething(x: any): any; }'
},
{name: _('/node_modules/test-package/index.metadata.json'), contents: 'DUMMY DATA'},
]);
expect(() => mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['main'],
}))
.not.toThrow();
});
it('should not be able to evaluate code in external packages when no .d.ts files are present',
() => {
loadTestFiles([
{
name: _(`/node_modules/external/package.json`),
contents: `{"name": "external", "main": "./index.js"}`,
},
{
name: _(`/node_modules/external/index.js`),
contents: `
export const selector = 'my-selector';
`,
},
]);
compileIntoApf('test-package', {
'/index.ts': `
import {NgModule, Component} from '@angular/core';
import {selector} from 'external';
@Component({
selector,
template: ''
})
export class FooComponent {
}
@NgModule({
declarations: [FooComponent],
})
export class FooModule {}
`,
});
try {
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015', 'esm5'],
});
fail('should have thrown');
} catch (e) {
expect(e.message).toContain(
'Failed to compile entry-point test-package (esm2015 as esm2015) due to compilation errors:');
expect(e.message).toContain('NG1010');
expect(e.message).toContain('selector must be a string');
}
});
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {Injectable, ɵɵdefineInjectable} from '@angular/core';
export const TestClassToken = 'TestClassToken';
@Injectable({providedIn: 'module'})
export class TestClass {
static ɵprov = ɵɵdefineInjectable({ factory: () => {}, token: TestClassToken, providedIn: "module" });
}
`,
});
const before = fs.readFile(_(`/node_modules/test-package/index.js`));
const originalProp = /ɵprov[^;]+/.exec(before)![0];
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const after = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(before).toContain(originalProp);
expect(countOccurrences(before, 'ɵprov')).toEqual(1);
expect(countOccurrences(before, 'ɵfac')).toEqual(0);
expect(after).toContain(originalProp);
expect(countOccurrences(after, 'ɵprov')).toEqual(1);
expect(countOccurrences(after, 'ɵfac')).toEqual(1);
});
// This is necessary to ensure XPipeDef.fac is defined when delegated from injectable def
it('should always generate factory def (fac) before injectable def (prov)', () => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {Injectable, Pipe, PipeTransform} from '@angular/core';
@Injectable()
@Pipe({
name: 'myTestPipe'
})
export class TestClass implements PipeTransform {
transform(value: any) { return value; }
}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents)
.toContain(
`TestClass.ɵfac = function TestClass_Factory(t) { return new (t || TestClass)(); };\n` +
`TestClass.ɵpipe = ɵngcc0.ɵɵdefinePipe({ name: "myTestPipe", type: TestClass, pure: true });\n` +
`TestClass.ɵprov = ɵngcc0.ɵɵdefineInjectable({`);
});
// https://github.com/angular/angular/issues/38883
it('should recognize ChangeDetectorRef as special symbol for pipes', () => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {ChangeDetectorRef, Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'myTestPipe'
})
export class TestClass implements PipeTransform {
constructor(cdr: ChangeDetectorRef) {}
transform(value: any) { return value; }
}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents)
.toContain(
`TestClass.ɵfac = function TestClass_Factory(t) { ` +
`return new (t || TestClass)(ɵngcc0.ɵɵinjectPipeChangeDetectorRef()); };`);
});
it('should use the correct type name in typings files when an export has a different name in source files',
() => {
// We need to make sure that changes to the typings files use the correct name
// static ɵprov: ɵngcc0.ɵɵInjectableDef<ɵangular_packages_common_common_a>;
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common',
propertiesToConsider: ['esm2015']
});
// In `@angular/common` the `BrowserPlatformLocation` class gets exported as something like
// `ɵangular_packages_common_common_a`.
const jsContents = fs.readFile(_(`/node_modules/@angular/common/fesm2015/common.js`));
const exportedNameMatch =
jsContents.match(/export.* BrowserPlatformLocation as ([^ ,}]+)/);
if (exportedNameMatch === null) {
return fail(
'Expected `/node_modules/@angular/common/fesm2015/common.js` to export `BrowserPlatformLocation` via an alias');
}
const exportedName = exportedNameMatch[1];
// We need to make sure that the flat typings file exports this directly
const dtsContents = fs.readFile(_('/node_modules/@angular/common/common.d.ts'));
expect(dtsContents)
.toContain(`export declare class ${exportedName} extends PlatformLocation`);
// And that ngcc's modifications to that class use the correct (exported) name
expect(dtsContents).toContain(`static ɵfac: ɵngcc0.ɵɵFactoryDef<${exportedName}, never>`);
});
it('should include constructor metadata in factory definitions', () => {
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common',
propertiesToConsider: ['esm2015']
});
const dtsContents = fs.readFile(_('/node_modules/@angular/common/common.d.ts'));
expect(dtsContents)
.toContain(
`static ɵfac: ɵngcc0.ɵɵFactoryDef`);
});
it('should add generic type for ModuleWithProviders and generate exports for private modules',
() => {
compileIntoApf('test-package', {
'/index.ts': `
import {ModuleWithProviders} from '@angular/core';
import {InternalFooModule} from './internal';
export class FooModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: InternalFooModule,
};
}
}
`,
'/internal.ts': `
import {NgModule} from '@angular/core';
@NgModule()
export class InternalFooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015', 'esm5', 'module'],
});
// The .d.ts where FooModule is declared should have a generic type added
const dtsContents = fs.readFile(_(`/node_modules/test-package/src/index.d.ts`));
expect(dtsContents).toContain(`import * as ɵngcc0 from './internal';`);
expect(dtsContents)
.toContain(`static forRoot(): ModuleWithProviders<ɵngcc0.InternalFooModule>`);
// The public facing .d.ts should export the InternalFooModule
const entryDtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
expect(entryDtsContents).toContain(`export {InternalFooModule} from './src/internal';`);
// The esm2015 index source should export the InternalFooModule
const esm2015Contents = fs.readFile(_(`/node_modules/test-package/esm2015/index.js`));
expect(esm2015Contents).toContain(`export {InternalFooModule} from './src/internal';`);
// The esm5 index source should also export the InternalFooModule
const esm5Contents = fs.readFile(_(`/node_modules/test-package/esm5/index.js`));
expect(esm5Contents).toContain(`export {InternalFooModule} from './src/internal';`);
});
it('should use `$localize` calls rather than tagged templates in ES5 generated code', () => {
setupAngularCoreEsm5();
compileIntoFlatEs5Package('test-package', {
'/index.ts': `
import {Component, Input, NgModule} from '@angular/core';
@Component({
selector: '[foo]',
template: 'A message
'
})
export class FooComponent {
}
@NgModule({
declarations: [FooComponent],
})
export class FooModule {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm5'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents).not.toMatch(/\$localize\s*`/);
expect(jsContents)
.toMatch(
/\$localize\(ɵngcc\d+\.__makeTemplateObject\(\[":some:`description`\\u241Fefc92f285b3c24b083a8a594f62c7fccf3118766\\u241F3806630072763809030:A message"], \[":some\\\\:\\\\`description\\\\`\\u241Fefc92f285b3c24b083a8a594f62c7fccf3118766\\u241F3806630072763809030:A message"]\)\);/);
});
describe('in async mode', () => {
it('should run ngcc without errors for fesm2015', async () => {
const promise = mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['fesm2015'],
async: true,
});
expect(promise).toEqual(jasmine.any(Promise));
await promise;
});
it('should reject, if some of the entry-points are unprocessable', async () => {
const createEntryPoint = (name: string, prop: EntryPointJsonProperty): TestFile[] => {
return [
{
name: _(`/dist/${name}/package.json`),
contents: `{"name": "${name}", "typings": "./index.d.ts", "${prop}": "./index.js"}`,
},
{name: _(`/dist/${name}/index.js`), contents: 'var DUMMY_DATA = true;'},
{name: _(`/dist/${name}/index.d.ts`), contents: 'export type DummyData = boolean;'},
{name: _(`/dist/${name}/index.metadata.json`), contents: 'DUMMY DATA'},
];
};
loadTestFiles([
...createEntryPoint('processable-1', 'es2015'),
...createEntryPoint('unprocessable-2', 'main'),
...createEntryPoint('unprocessable-3', 'main'),
]);
const promise = mainNgcc({
basePath: '/dist',
propertiesToConsider: ['es2015', 'fesm5', 'module'],
logger: new MockLogger(),
async: true,
});
await promise.then(
() => Promise.reject('Expected promise to be rejected.'),
err => expect(err).toEqual(new Error(
'Unable to process any formats for the following entry-points (tried es2015, fesm5, module): \n' +
` - ${_('/dist/unprocessable-2')}\n` +
` - ${_('/dist/unprocessable-3')}`)));
});
it('should reject, if an error happens during processing', async () => {
spyOn(Transformer.prototype, 'transform').and.throwError('Test error.');
const promise = mainNgcc({
basePath: '/dist',
targetEntryPointPath: 'local-package',
propertiesToConsider: ['main', 'es2015'],
logger: new MockLogger(),
async: true,
});
await promise.then(
() => Promise.reject('Expected promise to be rejected.'),
err => expect(err).toEqual(new Error('Test error.')));
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toBeUndefined();
});
});
describe('with targetEntryPointPath', () => {
it('should only compile the given package entry-point (and its dependencies).', () => {
mainNgcc({basePath: '/node_modules', targetEntryPointPath: '@angular/common/http/testing'});
expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `common/http` is a dependency of `common/http/testing`, so is compiled.
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `core` is a dependency of `common/http`, so is compiled.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common` is a private (only in .js not .d.ts) dependency so is compiled.
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common/testing` is not a dependency so is not compiled.
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toBeUndefined();
});
it('should not mark a non-Angular package as processed if it is the target', () => {
mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package'});
// * `test-package` has no Angular and is not marked as processed.
expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toBeUndefined();
// * `core` is a dependency of `test-package`, but it is also not processed, since
// `test-package` was not processed.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
});
it('should not mark a non-Angular package as processed if it is a dependency', () => {
// `test-package-user` is a valid Angular package that depends upon `test-package`.
loadTestFiles([
{
name: _('/node_modules/test-package-user/package.json'),
contents:
'{"name": "test-package-user", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package-user/index.js'),
contents: 'import * as x from \'test-package\';'
},
{
name: _('/node_modules/test-package-user/index.d.ts'),
contents: 'import * as x from \'test-package\';'
},
{name: _('/node_modules/test-package-user/index.metadata.json'), contents: 'DUMMY DATA'},
]);
mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'test-package-user'});
// * `test-package-user` is processed because it is compiled by Angular
expect(loadPackage('test-package-user').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// * `test-package` is a dependency of `test-package-user` but has not been compiled by
// Angular, and so is not marked as processed
expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toBeUndefined();
// * `core` is a dependency of `test-package`, but it is not processed, because
// `test-package` was not processed.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined();
});
it('should report an error if a dependency of the target does not exist', () => {
expect(() => {
mainNgcc({basePath: '/node_modules', targetEntryPointPath: 'invalid-package'});
})
.toThrowError(
'The target entry-point "invalid-package" has missing dependencies:\n - @angular/missing\n');
});
});
describe('early skipping of target entry-point', () => {
describe('[compileAllFormats === true]', () => {
it('should skip all processing if all the properties are marked as processed', () => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', SUPPORTED_FORMAT_PROPERTIES);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
logger,
});
expect(logger.logs.debug).toContain([
'The target entry-point has already been processed'
]);
});
it('should process the target if any `propertyToConsider` is not marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015', 'fesm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
propertiesToConsider: ['fesm2015', 'main', 'esm2015'],
logger,
});
expect(logger.logs.debug).not.toContain([
'The target entry-point has already been processed'
]);
});
});
describe('[compileAllFormats === false]', () => {
it('should process the target if the first matching `propertyToConsider` is not marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
propertiesToConsider: ['main', 'esm2015'],
compileAllFormats: false,
logger,
});
expect(logger.logs.debug).not.toContain([
'The target entry-point has already been processed'
]);
});
it('should skip all processing if the first matching `propertyToConsider` is marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
// Simulate a property that does not exist on the package.json and will be ignored.
propertiesToConsider: ['missing', 'esm2015', 'esm5'],
compileAllFormats: false,
logger,
});
expect(logger.logs.debug).toContain([
'The target entry-point has already been processed'
]);
});
});
it('should skip all processing if the first matching `propertyToConsider` is marked as processed',
() => {
const logger = new MockLogger();
markPropertiesAsProcessed('@angular/common/http/testing', ['esm2015']);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: '@angular/common/http/testing',
// Simulate a property that does not exist on the package.json and will be ignored.
propertiesToConsider: ['missing', 'esm2015', 'esm5'],
compileAllFormats: false,
logger,
});
expect(logger.logs.debug).toContain([
'The target entry-point has already been processed'
]);
});
});
function markPropertiesAsProcessed(packagePath: string, properties: EntryPointJsonProperty[]) {
const basePath = _('/node_modules');
const targetPackageJsonPath = join(basePath, packagePath, 'package.json');
const targetPackage = loadPackage(packagePath);
markAsProcessed(
pkgJsonUpdater, targetPackage, targetPackageJsonPath, ['typings', ...properties]);
}
describe('with findEntryPointsFromTsConfigProgram', () => {
it('should only compile the package entry-points (and their dependencies) reachable from the program in tsconfig.json.',
() => {
mainNgcc({basePath: '/node_modules', findEntryPointsFromTsConfigProgram: true});
// * `common/testing` is a dependency of `./y`, so is compiled.
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `common/http` is a dependency of `./x`, so is compiled.
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `core` is a dependency of `common/http`, so is compiled.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual(STANDARD_MARKERS);
// * `common` is a private (only in .js not .d.ts) dependency so is compiled.
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__)
.toEqual(STANDARD_MARKERS);
// * `common/http/testing` is not a dependency of the program so is not compiled.
expect(loadPackage('@angular/common/http/testing').__processed_by_ivy_ngcc__)
.toBeUndefined();
});
});
it('should clean up outdated artifacts', () => {
compileIntoFlatEs2015Package('test-package', {
'index.ts': `
import {Directive} from '@angular/core';
@Directive({selector: '[foo]'})
export class FooDirective {
}
`,
});
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm2015'],
logger: new MockLogger(),
});
// Now hack the files to look like it was processed by an outdated version of ngcc
const packageJson = loadPackage('test-package', _('/node_modules'));
packageJson.__processed_by_ivy_ngcc__!.typings = '8.0.0';
packageJson.main_ivy_ngcc = '__ivy_ngcc__/main.js';
fs.writeFile(_('/node_modules/test-package/package.json'), JSON.stringify(packageJson));
fs.writeFile(_('/node_modules/test-package/x.js'), 'processed content');
fs.writeFile(_('/node_modules/test-package/x.js.__ivy_ngcc_bak'), 'original content');
fs.ensureDir(_('/node_modules/test-package/__ivy_ngcc__/foo'));
// Now run ngcc again to see that it cleans out the outdated artifacts
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm2015'],
logger: new MockLogger(),
});
const newPackageJson = loadPackage('test-package', _('/node_modules'));
expect(newPackageJson.__processed_by_ivy_ngcc__).toEqual({
esm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(newPackageJson.module_ivy_ngcc).toBeUndefined();
expect(fs.exists(_('/node_modules/test-package/x.js'))).toBe(true);
expect(fs.exists(_('/node_modules/test-package/x.js.__ivy_ngcc_bak'))).toBe(false);
expect(fs.readFile(_('/node_modules/test-package/x.js'))).toEqual('original content');
expect(fs.exists(_('/node_modules/test-package/__ivy_ngcc__'))).toBe(false);
});
describe('with propertiesToConsider', () => {
it('should complain if none of the properties in the `propertiesToConsider` list is supported',
() => {
const propertiesToConsider = ['es1337', 'fesm42'];
const errorMessage =
'No supported format property to consider among [es1337, fesm42]. Supported ' +
'properties: fesm2015, fesm5, es2015, esm2015, esm5, main, module, browser';
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider}))
.toThrowError(errorMessage);
});
it('should only compile the entry-point formats given in the `propertiesToConsider` list',
() => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['main', 'module'],
logger: new MockLogger(),
});
// The ES2015 formats are not compiled as they are not in `propertiesToConsider`.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
// `module` and `es2015` are aliases of `fesm2015`.
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
// `module` and `es2015` are aliases of `fesm2015`.
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
// `module` and `es2015` are aliases for `fesm2015`.
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
// `module` and `es2015` are aliases for `fesm2015`.
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should mark all matching properties as processed in order not to compile them on a subsequent run',
() => {
const logger = new MockLogger();
const logs = logger.logs.debug;
// `fesm2015` and `es2015` map to the same file: `./fesm2015/common.js`
mainNgcc({
basePath: '/node_modules/@angular/common',
propertiesToConsider: ['fesm2015'],
logger,
});
expect(logs).not.toContain(['Skipping @angular/common : es2015 (already compiled).']);
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
// `module` and `es2015` are aliases of `fesm2015`.
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// Now, compiling `es2015` should be a no-op.
mainNgcc({
basePath: '/node_modules/@angular/common',
propertiesToConsider: ['es2015'],
logger,
});
expect(logs).toContain(['Skipping @angular/common : es2015 (already compiled).']);
});
});
describe('with compileAllFormats set to false', () => {
it('should only compile the first matching format', () => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['module', 'fesm2015', 'main'],
compileAllFormats: false,
logger: new MockLogger(),
});
// * In the Angular packages fesm2015, module and `es2015` have the same
// underlying format, so both are marked as compiled.
// * The `main` is not compiled because we stopped after the `fesm2015` format.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
fesm2015: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
fesm2015: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
fesm2015: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
fesm2015: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should cope with compiling the same entry-point multiple times with different formats',
() => {
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['main'],
compileAllFormats: false,
logger: new MockLogger(),
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// If ngcc tries to write out the typings files again, this will throw an exception.
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm2015'],
compileAllFormats: false,
logger: new MockLogger(),
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
esm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
describe('with createNewEntryPointFormats', () => {
it('should create new files rather than overwriting the originals', () => {
const ANGULAR_CORE_IMPORT_REGEX = /import \* as ɵngcc\d+ from '@angular\/core';/;
mainNgcc({
basePath: '/node_modules',
createNewEntryPointFormats: true,
propertiesToConsider: ['esm2015'],
logger: new MockLogger(),
});
// Updates the package.json
expect(loadPackage('@angular/common').esm2015).toEqual('./esm2015/common.js');
expect((loadPackage('@angular/common') as any).esm2015_ivy_ngcc)
.toEqual('__ivy_ngcc__/esm2015/common.js');
// Doesn't touch original files
expect(fs.readFile(_(`/node_modules/@angular/common/esm2015/src/common_module.js`)))
.not.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Or create a backup of the original
expect(fs.exists(
_(`/node_modules/@angular/common/esm2015/src/common_module.js.__ivy_ngcc_bak`)))
.toBe(false);
// Creates new files
expect(fs.readFile(
_(`/node_modules/@angular/common/__ivy_ngcc__/esm2015/src/common_module.js`)))
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
// Copies over files (unchanged) that did not need compiling
expect(fs.exists(_(`/node_modules/@angular/common/__ivy_ngcc__/esm2015/src/version.js`)))
.toBeTrue();
expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm2015/src/version.js`)))
.toEqual(fs.readFile(_(`/node_modules/@angular/common/esm2015/src/version.js`)));
// Overwrites .d.ts files (as usual)
expect(fs.readFile(_(`/node_modules/@angular/common/common.d.ts`)))
.toMatch(ANGULAR_CORE_IMPORT_REGEX);
expect(fs.exists(_(`/node_modules/@angular/common/common.d.ts.__ivy_ngcc_bak`))).toBe(true);
});
it('should update `package.json` for all matching format properties', () => {
mainNgcc({
basePath: '/node_modules/@angular/core',
createNewEntryPointFormats: true,
propertiesToConsider: ['fesm2015', 'main'],
});
const pkg: any = loadPackage('@angular/core');
// `es2015` and `module` are aliases of `fesm2015`.
expect(pkg.fesm2015).toEqual('./fesm2015/core.js');
expect(pkg.es2015).toEqual('./fesm2015/core.js');
expect(pkg.module).toEqual('./fesm2015/core.js');
expect(pkg.fesm2015_ivy_ngcc).toEqual('__ivy_ngcc__/fesm2015/core.js');
expect(pkg.es2015_ivy_ngcc).toEqual('__ivy_ngcc__/fesm2015/core.js');
expect(pkg.module_ivy_ngcc).toEqual('__ivy_ngcc__/fesm2015/core.js');
expect(pkg.main).toEqual('./bundles/core.umd.js');
expect(pkg.main_ivy_ngcc).toEqual('__ivy_ngcc__/bundles/core.umd.js');
});
it('should update `package.json` deterministically (regardless of entry-point processing order)',
() => {
// Ensure formats are not marked as processed in `package.json` at the beginning.
let pkg = loadPackage('@angular/core');
expectNotToHaveProp(pkg, 'main_ivy_ngcc');
expectNotToHaveProp(pkg, 'esm2015_ivy_ngcc');
expectNotToHaveProp(pkg, 'fesm2015_ivy_ngcc');
expectNotToHaveProp(pkg, 'module_ivy_ngcc');
expectNotToHaveProp(pkg, '__processed_by_ivy_ngcc__');
// Process `fesm2015` and update `package.json`.
pkg = processFormatAndUpdatePackageJson('fesm2015');
expectNotToHaveProp(pkg, 'main_ivy_ngcc');
expectNotToHaveProp(pkg, 'esm2015_ivy_ngcc');
expectToHaveProp(pkg, 'fesm2015_ivy_ngcc');
expectToHaveProp(pkg, 'module_ivy_ngcc');
expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'fesm2015');
// Process `esm2015` and update `package.json`.
pkg = processFormatAndUpdatePackageJson('esm2015');
expectNotToHaveProp(pkg, 'main_ivy_ngcc');
expectToHaveProp(pkg, 'esm2015_ivy_ngcc');
expectToHaveProp(pkg, 'fesm2015_ivy_ngcc');
expectToHaveProp(pkg, 'module_ivy_ngcc');
expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'esm2015');
// Process `main` and update `package.json`.
pkg = processFormatAndUpdatePackageJson('main');
expectToHaveProp(pkg, 'main_ivy_ngcc');
expectToHaveProp(pkg, 'esm2015_ivy_ngcc');
expectToHaveProp(pkg, 'fesm2015_ivy_ngcc');
expectToHaveProp(pkg, 'module_ivy_ngcc');
expectToHaveProp(pkg.__processed_by_ivy_ngcc__!, 'main');
// Ensure the properties are in deterministic order (regardless of processing order).
const pkgKeys = stringifyKeys(pkg);
expect(pkgKeys).toContain('|main_ivy_ngcc|main|');
expect(pkgKeys).toContain('|fesm2015_ivy_ngcc|fesm2015|');
expect(pkgKeys).toContain('|esm2015_ivy_ngcc|esm2015|');
// NOTE:
// Along with the first format that is processed, the typings are processed as well.
// Also, once a property has been processed, alias properties as also marked as
// processed. Aliases properties are properties that point to the same entry-point file.
// For example:
// - `fesm2015` <=> `module <=> es2015`
expect(stringifyKeys(pkg.__processed_by_ivy_ngcc__!))
.toBe('|es2015|esm2015|fesm2015|main|module|typings|');
// Helpers
function expectNotToHaveProp(obj: object, prop: string) {
expect(obj.hasOwnProperty(prop))
.toBe(
false,
`Expected object not to have property '${prop}': ${
JSON.stringify(obj, null, 2)}`);
}
function expectToHaveProp(obj: object, prop: string) {
expect(obj.hasOwnProperty(prop))
.toBe(
true,
`Expected object to have property '${prop}': ${JSON.stringify(obj, null, 2)}`);
}
function processFormatAndUpdatePackageJson(formatProp: string) {
mainNgcc({
basePath: '/node_modules/@angular/core',
createNewEntryPointFormats: true,
propertiesToConsider: [formatProp],
});
return loadPackage('@angular/core');
}
function stringifyKeys(obj: object) {
return `|${Object.keys(obj).join('|')}|`;
}
});
});
describe('with ignoreEntryPointManifest', () => {
it('should not read the entry-point manifest file', () => {
// Ensure there is a lock-file. Otherwise the manifest will not be written
fs.writeFile(_('/yarn.lock'), 'DUMMY YARN LOCK FILE');
// Populate the manifest file
mainNgcc(
{basePath: '/node_modules', propertiesToConsider: ['main'], logger: new MockLogger()});
// Check that common/testing ES5 was processed
let commonTesting =
JSON.parse(fs.readFile(_('/node_modules/@angular/common/testing/package.json')));
expect(hasBeenProcessed(commonTesting, 'main')).toBe(true);
expect(hasBeenProcessed(commonTesting, 'esm2015')).toBe(false);
// Modify the manifest to test that is has no effect
let manifest: EntryPointManifestFile =
JSON.parse(fs.readFile(_('/node_modules/__ngcc_entry_points__.json')));
manifest.entryPointPaths =
manifest.entryPointPaths.filter(paths => paths[1] !== '@angular/common/testing');
fs.writeFile(_('/node_modules/__ngcc_entry_points__.json'), JSON.stringify(manifest));
// Now run ngcc again ignoring this manifest but trying to process ES2015, which are not yet
// processed.
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm2015'],
logger: new MockLogger(),
invalidateEntryPointManifest: true,
});
// Check that common/testing ES2015 is now processed, despite the manifest not listing it
commonTesting =
JSON.parse(fs.readFile(_('/node_modules/@angular/common/testing/package.json')));
expect(hasBeenProcessed(commonTesting, 'main')).toBe(true);
expect(hasBeenProcessed(commonTesting, 'esm2015')).toBe(true);
// Check that the newly computed manifest has written to disk, containing the path that we
// had removed earlier.
manifest = JSON.parse(fs.readFile(_('/node_modules/__ngcc_entry_points__.json')));
expect(manifest.entryPointPaths).toContain([
'@angular/common',
'@angular/common/testing',
[
_('/node_modules/@angular/core'), _('/node_modules/@angular/common'),
_('/node_modules/rxjs')
],
]);
});
});
describe('diagnostics', () => {
it('should fail with formatted diagnostics when an error diagnostic is produced, if targetEntryPointPath is provided',
() => {
loadTestFiles([
{
name: _('/node_modules/fatal-error/package.json'),
contents:
'{"name": "fatal-error", "es2015": "./index.js", "typings": "./index.d.ts"}',
},
{name: _('/node_modules/fatal-error/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/node_modules/fatal-error/index.js'),
contents: `
import {Component} from '@angular/core';
export class FatalError {}
FatalError.decorators = [
{type: Component, args: [{selector: 'fatal-error'}]}
];
`,
},
{
name: _('/node_modules/fatal-error/index.d.ts'),
contents: `
export declare class FatalError {}
`,
},
]);
try {
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'fatal-error',
propertiesToConsider: ['es2015']
});
fail('should have thrown');
} catch (e) {
expect(e.message).toContain(
'Failed to compile entry-point fatal-error (es2015 as esm2015) due to compilation errors:');
expect(e.message).toContain('NG2001');
expect(e.message).toContain('component is missing a template');
}
});
it('should not fail but log an error with formatted diagnostics when an error diagnostic is produced, if targetEntryPoint is not provided and errorOnFailedEntryPoint is false',
() => {
loadTestFiles([
{
name: _('/node_modules/fatal-error/package.json'),
contents:
'{"name": "fatal-error", "es2015": "./index.js", "typings": "./index.d.ts"}',
},
{name: _('/node_modules/fatal-error/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/node_modules/fatal-error/index.js'),
contents: `
import {Component} from '@angular/core';
export class FatalError {}
FatalError.decorators = [
{type: Component, args: [{selector: 'fatal-error'}]}
];`,
},
{
name: _('/node_modules/fatal-error/index.d.ts'),
contents: `export declare class FatalError {}`,
},
{
name: _('/node_modules/dependent/package.json'),
contents: '{"name": "dependent", "es2015": "./index.js", "typings": "./index.d.ts"}',
},
{name: _('/node_modules/dependent/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/node_modules/dependent/index.js'),
contents: `
import {Component} from '@angular/core';
import {FatalError} from 'fatal-error';
export class Dependent {}
Dependent.decorators = [
{type: Component, args: [{selector: 'dependent', template: ''}]}
];`,
},
{
name: _('/node_modules/dependent/index.d.ts'),
contents: `export declare class Dependent {}`,
},
{
name: _('/node_modules/independent/package.json'),
contents:
'{"name": "independent", "es2015": "./index.js", "typings": "./index.d.ts"}',
},
{name: _('/node_modules/independent/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/node_modules/independent/index.js'),
contents: `
import {Component} from '@angular/core';
export class Independent {}
Independent.decorators = [
{type: Component, args: [{selector: 'independent', template: ''}]}
];`,
},
{
name: _('/node_modules/independent/index.d.ts'),
contents: `export declare class Independent {}`,
},
]);
const logger = new MockLogger();
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['es2015'],
errorOnFailedEntryPoint: false,
logger,
});
expect(logger.logs.error.length).toEqual(1);
const message = logger.logs.error[0][0];
expect(message).toContain(
'Failed to compile entry-point fatal-error (es2015 as esm2015) due to compilation errors:');
expect(message).toContain('NG2001');
expect(message).toContain('component is missing a template');
expect(hasBeenProcessed(loadPackage('fatal-error', _('/node_modules')), 'es2015'))
.toBe(false);
expect(hasBeenProcessed(loadPackage('dependent', _('/node_modules')), 'es2015'))
.toBe(false);
expect(hasBeenProcessed(loadPackage('independent', _('/node_modules')), 'es2015'))
.toBe(true);
});
});
describe('logger', () => {
it('should log info message to the console by default', () => {
const consoleInfoSpy = spyOn(console, 'info');
mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']});
expect(consoleInfoSpy)
.toHaveBeenCalledWith('Compiling @angular/common/http : esm2015 as esm2015');
});
it('should use a custom logger if provided', () => {
const logger = new MockLogger();
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['esm2015'],
logger,
});
expect(logger.logs.info).toContain(['Compiling @angular/common/http : esm2015 as esm2015']);
});
});
describe('with pathMappings', () => {
it('should infer the @app pathMapping from a local tsconfig.json path', () => {
fs.writeFile(
_('/tsconfig.json'),
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
const logger = new MockLogger();
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015'], logger});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// The local-package-3 and local-package-4 will not be processed because there is no path
// mappings for `@x` and plain local imports.
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-3')}.`,
'It is missing required dependencies:\n - @x/local-package'
]);
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-4')}.`,
'It is missing required dependencies:\n - local-package'
]);
});
it('should read the @x pathMapping from a specified tsconfig.json path', () => {
fs.writeFile(
_('/tsconfig.app.json'),
JSON.stringify({compilerOptions: {paths: {'@x/*': ['dist/*']}, baseUrl: './'}}));
const logger = new MockLogger();
mainNgcc({
basePath: '/dist',
propertiesToConsider: ['es2015'],
tsConfigPath: _('/tsconfig.app.json'),
logger
});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// The local-package-2 and local-package-4 will not be processed because there is no path
// mappings for `@app` and plain local imports.
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-2')}.`,
'It is missing required dependencies:\n - @app/local-package'
]);
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-4')}.`,
'It is missing required dependencies:\n - local-package'
]);
});
it('should use the explicit `pathMappings`, ignoring the local tsconfig.json settings',
() => {
const logger = new MockLogger();
fs.writeFile(
_('/tsconfig.json'),
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
mainNgcc({
basePath: '/node_modules',
propertiesToConsider: ['es2015'],
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
logger
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
// `module` and `es2015` are aliases for `fesm2015`.
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('local-package-4', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// The local-package-2 and local-package-3 will not be processed because there is no path
// mappings for `@app` and `@x` local imports.
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-2')}.`,
'It is missing required dependencies:\n - @app/local-package'
]);
expect(loadPackage('local-package-3', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-3')}.`,
'It is missing required dependencies:\n - @x/local-package'
]);
});
it('should not use pathMappings from a local tsconfig.json path if tsConfigPath is null',
() => {
const logger = new MockLogger();
fs.writeFile(
_('/tsconfig.json'),
JSON.stringify({compilerOptions: {paths: {'@app/*': ['dist/*']}, baseUrl: './'}}));
mainNgcc({
basePath: '/dist',
propertiesToConsider: ['es2015'],
tsConfigPath: null,
logger,
});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
// Since the tsconfig is not loaded, the `@app/local-package` import in `local-package-2`
// is not path-mapped correctly, and so it fails to be processed.
expect(loadPackage('local-package-2', _('/dist')).__processed_by_ivy_ngcc__)
.toBeUndefined();
expect(logger.logs.debug).toContain([
`Invalid entry-point ${_('/dist/local-package-2')}.`,
'It is missing required dependencies:\n - @app/local-package'
]);
});
});
describe('whitespace preservation', () => {
it('should default not to preserve whitespace', () => {
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(fs.readFile(_('/dist/local-package/index.js')))
.toMatch(/ɵɵtext\(\d+, " Hello\\n"\);/);
});
it('should preserve whitespace if set in a loaded tsconfig.json', () => {
fs.writeFile(
_('/tsconfig.json'),
JSON.stringify({angularCompilerOptions: {preserveWhitespaces: true}}));
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(fs.readFile(_('/dist/local-package/index.js')))
.toMatch(/ɵɵtext\(\d+, "\\n Hello\\n"\);/);
});
it('should not preserve whitespace if set to false in a loaded tsconfig.json', () => {
fs.writeFile(
_('/tsconfig.json'),
JSON.stringify({angularCompilerOptions: {preserveWhitespaces: false}}));
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(fs.readFile(_('/dist/local-package/index.js')))
.toMatch(/ɵɵtext\(\d+, " Hello\\n"\);/);
});
});
describe('with Closure Compiler', () => {
it('should give closure annotated output with annotateForClosureCompiler: true', () => {
fs.writeFile(
_('/tsconfig.json'),
JSON.stringify({angularCompilerOptions: {annotateForClosureCompiler: true}}));
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
const jsContents = fs.readFile(_(`/dist/local-package/index.js`));
expect(jsContents).toContain('/** @nocollapse */\nAppComponent.ɵcmp =');
});
it('should default to not give closure annotated output', () => {
mainNgcc({basePath: '/dist', propertiesToConsider: ['es2015']});
const jsContents = fs.readFile(_(`/dist/local-package/index.js`));
expect(jsContents).not.toContain('@nocollapse');
});
});
describe('with configuration files', () => {
it('should process a configured deep-import as an entry-point', () => {
loadTestFiles([
{
name: _('/ngcc.config.js'),
contents: `module.exports = { packages: {
'deep_import': {
entryPoints: {
'./entry_point': { override: { typings: '../entry_point.d.ts', es2015: '../entry_point.js' } }
}
}
}};`,
},
{
name: _('/node_modules/deep_import/package.json'),
contents: '{"name": "deep_import", "es2015": "./index.js", "typings": "./index.d.ts"}',
},
{
name: _('/node_modules/deep_import/entry_point.js'),
contents: `
import {Component} from '@angular/core';
@Component({selector: 'entry-point'})
export class EntryPoint {}
`,
},
{
name: _('/node_modules/deep_import/entry_point.d.ts'),
contents: `
import {Component} from '@angular/core';
@Component({selector: 'entry-point'})
export class EntryPoint {}
`,
},
]);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'deep_import/entry_point',
propertiesToConsider: ['es2015']
});
// The containing package is not processed
expect(loadPackage('deep_import').__processed_by_ivy_ngcc__).toBeUndefined();
// But the configured entry-point and its dependency (@angular/core) are processed.
expect(loadPackage('deep_import/entry_point').__processed_by_ivy_ngcc__).toEqual({
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should not process ignored entry-points', () => {
loadTestFiles([
{
name: _('/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'@angular/core': {
entryPoints: {
'./testing': {ignore: true},
},
},
'@angular/common': {
entryPoints: {
'.': {ignore: true},
'./http': {override: {fesm2015: undefined}},
},
},
},
};
`,
},
]);
mainNgcc({basePath: '/node_modules', propertiesToConsider: ['es2015']});
// We process core but not core/testing.
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
fesm2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
expect(loadPackage('@angular/core/testing').__processed_by_ivy_ngcc__).toBeUndefined();
// We do not compile common but we do compile its sub-entry-points.
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toBeUndefined();
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
// `fesm2015` is not processed, because the ngcc config removes it.
// fesm2015: '0.0.0-PLACEHOLDER',
module: '0.0.0-PLACEHOLDER',
es2015: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
it('should support removing a format property by setting it to `undefined`', () => {
loadTestFiles([
{
name: _('/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'test-package': {
entryPoints: {
'.': {
override: {
fesm2015: undefined,
},
},
},
},
},
};
`,
},
{
name: _('/node_modules/test-package/package.json'),
contents: `
{
"name": "test-package",
"fesm2015": "./index.es2015.js",
"fesm5": "./index.es5.js",
"typings": "./index.d.ts"
}
`,
},
{
name: _('/node_modules/test-package/index.es5.js'),
contents: `
var TestService = (function () {
function TestService() {
}
return TestService;
}());
`,
},
{
name: _('/node_modules/test-package/index.d.js'),
contents: `
export declare class TestService {}
`,
},
]);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['fesm2015', 'fesm5'],
});
expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({
fesm5: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
});
});
describe('undecorated child class migration', () => {
it('should generate a directive definition with CopyDefinitionFeature for an undecorated child directive',
() => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {Directive, NgModule} from '@angular/core';
@Directive({
selector: '[base]',
exportAs: 'base1, base2',
})
export class BaseDir {}
export class DerivedDir extends BaseDir {}
@NgModule({
declarations: [DerivedDir],
})
export class Module {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents)
.toContain(
'DerivedDir.ɵdir = ɵngcc0.ɵɵdefineDirective({ type: DerivedDir, ' +
'selectors: [["", "base", ""]], exportAs: ["base1", "base2"], ' +
'features: [ɵngcc0.ɵɵInheritDefinitionFeature, ɵngcc0.ɵɵCopyDefinitionFeature] });');
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
expect(dtsContents)
.toContain(
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;');
});
it('should generate a component definition with CopyDefinitionFeature for an undecorated child component',
() => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: '[base]',
template: 'This is the base template',
})
export class BaseCmp {}
export class DerivedCmp extends BaseCmp {}
@NgModule({
declarations: [DerivedCmp],
})
export class Module {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents).toContain('DerivedCmp.ɵcmp = ɵngcc0.ɵɵdefineComponent');
expect(jsContents)
.toContain(
'features: [ɵngcc0.ɵɵInheritDefinitionFeature, ɵngcc0.ɵɵCopyDefinitionFeature]');
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
expect(dtsContents)
.toContain(
'static ɵcmp: ɵngcc0.ɵɵComponentDefWithMeta;');
});
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',
() => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {Directive, NgModule} from '@angular/core';
@Directive({
selector: '[base]',
})
export class BaseDir {}
export class DerivedDir1 extends BaseDir {}
export class DerivedDir2 extends DerivedDir1 {}
export class DerivedDir3 extends DerivedDir2 {}
@NgModule({
declarations: [DerivedDir3],
})
export class Module {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
expect(dtsContents)
.toContain(
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;');
expect(dtsContents)
.toContain(
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;');
expect(dtsContents)
.toContain(
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta;');
});
});
describe('aliasing re-exports in commonjs', () => {
it('should add re-exports to commonjs files', () => {
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: `
{
"name": "test-package",
"main": "./index.js",
"typings": "./index.d.ts"
}
`,
},
{
name: _('/node_modules/test-package/index.js'),
contents: `
var __export = null;
__export(require("./module"));
`,
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents: `
export * from "./module";
`,
},
{
name: _('/node_modules/test-package/index.metadata.json'),
contents: '{}',
},
{
name: _('/node_modules/test-package/module.js'),
contents: `
var __decorate = null;
var core_1 = require("@angular/core");
var directive_1 = require("./directive");
var LocalDir = /** @class */ (function () {
function LocalDir() {
}
LocalDir = __decorate([
core_1.Directive({
selector: '[local]',
})
], LocalDir);
return LocalDir;
}());
var FooModule = /** @class */ (function () {
function FooModule() {
}
FooModule = __decorate([
core_1.NgModule({
declarations: [directive_1.Foo, LocalDir],
exports: [directive_1.Foo, LocalDir],
})
], FooModule);
return FooModule;
}());
exports.LocalDir = LocalDir;
exports.FooModule = FooModule;
`,
},
{
name: _('/node_modules/test-package/module.d.ts'),
contents: `
export declare class LocalDir {}
export declare class FooModule {}
`,
},
{
name: _('/node_modules/test-package/module.metadata.json'),
contents: '{}',
},
{
name: _('/node_modules/test-package/directive.js'),
contents: `
var __decorate = null;
var core_1 = require("@angular/core");
var Foo = /** @class */ (function () {
function Foo() {
}
Foo = __decorate([
core_1.Directive({
selector: '[foo]',
})
], Foo);
return Foo;
}());
exports.Foo = Foo;
`,
},
{
name: _('/node_modules/test-package/directive.d.ts'),
contents: `
export declare class Foo {}
`,
},
{
name: _('/node_modules/test-package/directive.metadata.json'),
contents: '{}',
},
{
name: _('/ngcc.config.js'),
contents: `
module.exports = {
packages: {
'test-package': {
entryPoints: {
'.': {
generateDeepReexports: true
},
},
},
},
};
`,
}
]);
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['main'],
});
expect(loadPackage('test-package').__processed_by_ivy_ngcc__).toEqual({
main: '0.0.0-PLACEHOLDER',
typings: '0.0.0-PLACEHOLDER',
});
const jsContents = fs.readFile(_(`/node_modules/test-package/module.js`));
const dtsContents = fs.readFile(_(`/node_modules/test-package/module.d.ts`));
expect(jsContents).toContain(`var ɵngcc1 = require('./directive');`);
expect(jsContents).toContain('exports.ɵngExportɵFooModuleɵFoo = ɵngcc1.Foo;');
expect(dtsContents)
.toContain(`export {Foo as ɵngExportɵFooModuleɵFoo} from './directive';`);
expect(dtsContents.match(/ɵngExportɵFooModuleɵFoo/g)!.length).toBe(1);
expect(dtsContents).not.toContain(`ɵngExportɵFooModuleɵLocalDir`);
});
});
describe('legacy message ids', () => {
it('should render legacy message ids when compiling i18n tags in component templates', () => {
compileIntoApf('test-package', {
'/index.ts': `
import {Component} from '@angular/core';
@Component({
selector: '[base]',
template: 'Some message
'
})
export class AppComponent {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/esm2015/src/index.js`));
expect(jsContents)
.toContain(
'$localize `:␟888aea0e46f7e9dddbd95fc1ef380a3ff70ada9d␟1812794354835616626:Some message');
});
it('should not render legacy message ids when compiling i18n tags in component templates if `enableI18nLegacyMessageIdFormat` is false',
() => {
compileIntoApf('test-package', {
'/index.ts': `
import {Component} from '@angular/core';
@Component({
selector: '[base]',
template: 'Some message
'
})
export class AppComponent {}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
enableI18nLegacyMessageIdFormat: false,
});
const jsContents = fs.readFile(_(`/node_modules/test-package/esm2015/src/index.js`));
expect(jsContents).not.toContain('␟888aea0e46f7e9dddbd95fc1ef380a3ff70ada9d');
expect(jsContents).not.toContain('␟1812794354835616626');
expect(jsContents).not.toContain('␟');
});
});
function loadPackage(
packageName: string, basePath: AbsoluteFsPath = _('/node_modules')): EntryPointPackageJson {
return JSON.parse(fs.readFile(fs.resolve(basePath, packageName, 'package.json')));
}
function initMockFileSystem(fs: FileSystem, testFiles: Folder) {
if (fs instanceof MockFileSystem) {
fs.init(testFiles);
fs.ensureDir(fs.dirname(getLockFilePath(fs)));
}
// a random test package that no metadata.json file so not compiled by Angular.
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: '{"name": "test-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package/index.js'),
contents:
'import {AppModule} from "@angular/common"; export class MyApp extends AppModule {};'
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents:
'import {AppModule} from "@angular/common"; export declare class MyApp extends AppModule;'
},
]);
// Angular packages that have been built locally and stored in the `dist` directory.
loadTestFiles([
{
name: _('/dist/local-package/package.json'),
contents: '{"name": "local-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/local-package/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/dist/local-package/index.js'),
contents:
`import {Component} from '@angular/core';\nexport class AppComponent {};\nAppComponent.decorators = [\n{ type: Component, args: [{selector: 'app', template: '\\n Hello\\n
'}] }\n];`
},
{
name: _('/dist/local-package/index.d.ts'),
contents: `export declare class AppComponent {};`
},
// local-package-2 depends upon local-package, via an `@app` aliased import.
{
name: _('/dist/local-package-2/package.json'),
contents: '{"name": "local-package-2", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/local-package-2/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/dist/local-package-2/index.js'),
contents:
`import {Component} from '@angular/core';\nexport {AppComponent} from '@app/local-package';`
},
{
name: _('/dist/local-package-2/index.d.ts'),
contents:
`import {Component} from '@angular/core';\nexport {AppComponent} from '@app/local-package';`
},
// local-package-3 depends upon local-package, via an `@x` aliased import.
{
name: _('/dist/local-package-3/package.json'),
contents: '{"name": "local-package-3", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/local-package-3/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/dist/local-package-3/index.js'),
contents:
`import {Component} from '@angular/core';\nexport {AppComponent} from '@x/local-package';`
},
{
name: _('/dist/local-package-3/index.d.ts'),
contents:
`import {Component} from '@angular/core';\nexport {AppComponent} from '@x/local-package';`
},
// local-package-4 depends upon local-package, via a plain import.
{
name: _('/dist/local-package-4/package.json'),
contents: '{"name": "local-package-", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{name: _('/dist/local-package-4/index.metadata.json'), contents: 'DUMMY DATA'},
{
name: _('/dist/local-package-4/index.js'),
contents:
`import {Component} from '@angular/core';\nexport {AppComponent} from 'local-package';`
},
{
name: _('/dist/local-package-4/index.d.ts'),
contents:
`import {Component} from '@angular/core';\nexport {AppComponent} from 'local-package';`
},
]);
// An Angular package that has a missing dependency
loadTestFiles([
{
name: _('/node_modules/invalid-package/package.json'),
contents: '{"name": "invalid-package", "es2015": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/invalid-package/index.js'),
contents: `
import {AppModule} from "@angular/missing";
import {Component} from '@angular/core';
export class AppComponent {};
AppComponent.decorators = [
{ type: Component, args: [{selector: 'app', template: 'Hello
'}] }
];
`
},
{
name: _('/node_modules/invalid-package/index.d.ts'),
contents: `export declare class AppComponent {}`
},
{name: _('/node_modules/invalid-package/index.metadata.json'), contents: 'DUMMY DATA'},
]);
// A sample application that imports entry-points
loadTestFiles([
{name: _('/tsconfig.json'), contents: '{"files": ["src/index.ts"]}'},
{name: _('/src/index.ts'), contents: `import {X} from './x';\nimport {Y} from './y';`},
{name: _('/src/x.ts'), contents: `import '@angular/common/http';\nexport class X {}`},
{
name: _('/src/y.ts'),
contents: `import * as t from '@angular/common/testing';\n export class Y {}`
},
]);
}
});
});
function countOccurrences(haystack: string, needle: string): number {
const matches = haystack.match(new RegExp(needle, 'g'));
return matches !== null ? matches.length : 0;
}