1715 lines
66 KiB
TypeScript
1715 lines
66 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
|
|
*/
|
|
|
|
/// <reference types="node" />
|
|
import * as os from 'os';
|
|
import {AbsoluteFsPath, FileSystem, absoluteFrom, getFileSystem, join} from '../../../src/ngtsc/file_system';
|
|
import {Folder, MockFileSystem, TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
|
import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers';
|
|
import {getLockFilePath} from '../../src/locking/lock_file';
|
|
import {mainNgcc} from '../../src/main';
|
|
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 {MockLogger} from '../helpers/mock_logger';
|
|
import {compileIntoApf, compileIntoFlatEs5Package} from './util';
|
|
|
|
const testFiles = loadStandardTestFiles({fakeCore: false, rxjs: true});
|
|
|
|
runInEachFileSystem(() => {
|
|
describe('ngcc main()', () => {
|
|
let _: typeof absoluteFrom;
|
|
let fs: FileSystem;
|
|
let pkgJsonUpdater: PackageJsonUpdater;
|
|
|
|
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'}]);
|
|
});
|
|
|
|
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', () => {
|
|
expect(() => mainNgcc({
|
|
basePath: '/node_modules',
|
|
propertiesToConsider: ['esm5'],
|
|
logger: new MockLogger(),
|
|
}))
|
|
.not.toThrow();
|
|
});
|
|
|
|
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', () => {
|
|
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: ['module'],
|
|
});
|
|
|
|
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)`,
|
|
() => {
|
|
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)`,
|
|
() => {
|
|
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', () => {
|
|
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: ['module'],
|
|
});
|
|
|
|
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<MyOtherModule> {
|
|
return {ngModule: MyOtherModule};
|
|
}
|
|
}
|
|
`
|
|
});
|
|
|
|
compileIntoFlatEs5Package('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: ['module'],
|
|
});
|
|
|
|
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 add ɵfac but not duplicate ɵprov properties on injectables', () => {
|
|
compileIntoFlatEs5Package('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: ['module'],
|
|
});
|
|
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)', () => {
|
|
compileIntoFlatEs5Package('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: ['module'],
|
|
});
|
|
|
|
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({`);
|
|
});
|
|
|
|
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}>`);
|
|
});
|
|
|
|
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', () => {
|
|
compileIntoFlatEs5Package('test-package', {
|
|
'/index.ts': `
|
|
import {Component, Input, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: '[foo]',
|
|
template: '<div i18n="some:\`description\`">A message</div>'
|
|
})
|
|
export class FooComponent {
|
|
}
|
|
|
|
@NgModule({
|
|
declarations: [FooComponent],
|
|
})
|
|
export class FooModule {}
|
|
`,
|
|
});
|
|
|
|
mainNgcc({
|
|
basePath: '/node_modules',
|
|
targetEntryPointPath: 'test-package',
|
|
propertiesToConsider: ['module'],
|
|
});
|
|
|
|
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).', () => {
|
|
const STANDARD_MARKERS = {
|
|
main: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
es2015: '0.0.0-PLACEHOLDER',
|
|
esm5: '0.0.0-PLACEHOLDER',
|
|
esm2015: '0.0.0-PLACEHOLDER',
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
fesm2015: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
};
|
|
|
|
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', 'esm5', '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: ['esm5', '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]);
|
|
}
|
|
|
|
it('should clean up outdated artifacts', () => {
|
|
compileIntoFlatEs5Package('test-package', {
|
|
'index.ts': `
|
|
import {Directive} from '@angular/core';
|
|
|
|
@Directive({selector: '[foo]'})
|
|
export class FooDirective {
|
|
}
|
|
`,
|
|
});
|
|
mainNgcc({
|
|
basePath: '/node_modules',
|
|
propertiesToConsider: ['module'],
|
|
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: ['module'],
|
|
logger: new MockLogger(),
|
|
});
|
|
const newPackageJson = loadPackage('test-package', _('/node_modules'));
|
|
expect(newPackageJson.__processed_by_ivy_ngcc__).toEqual({
|
|
module: '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';
|
|
|
|
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', 'esm5', 'module', 'fesm5'],
|
|
logger: new MockLogger(),
|
|
|
|
});
|
|
|
|
// The ES2015 formats are not compiled as they are not in `propertiesToConsider`.
|
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
|
esm5: '0.0.0-PLACEHOLDER',
|
|
main: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
});
|
|
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
|
|
esm5: '0.0.0-PLACEHOLDER',
|
|
main: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
});
|
|
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
|
|
esm5: '0.0.0-PLACEHOLDER',
|
|
main: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
});
|
|
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
|
|
esm5: '0.0.0-PLACEHOLDER',
|
|
main: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
fesm5: '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({
|
|
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', 'fesm5', 'esm5'],
|
|
compileAllFormats: false,
|
|
logger: new MockLogger(),
|
|
});
|
|
// * In the Angular packages fesm5 and module have the same underlying format,
|
|
// so both are marked as compiled.
|
|
// * The `esm5` is not compiled because we stopped after the `fesm5` format.
|
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
});
|
|
expect(loadPackage('@angular/common').__processed_by_ivy_ngcc__).toEqual({
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
});
|
|
expect(loadPackage('@angular/common/testing').__processed_by_ivy_ngcc__).toEqual({
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
module: '0.0.0-PLACEHOLDER',
|
|
typings: '0.0.0-PLACEHOLDER',
|
|
});
|
|
expect(loadPackage('@angular/common/http').__processed_by_ivy_ngcc__).toEqual({
|
|
fesm5: '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: ['module'],
|
|
compileAllFormats: false,
|
|
logger: new MockLogger(),
|
|
|
|
});
|
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
module: '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: ['esm5'],
|
|
compileAllFormats: false,
|
|
logger: new MockLogger(),
|
|
});
|
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
|
esm5: '0.0.0-PLACEHOLDER',
|
|
fesm5: '0.0.0-PLACEHOLDER',
|
|
module: '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: ['esm5'],
|
|
logger: new MockLogger(),
|
|
|
|
});
|
|
|
|
// Updates the package.json
|
|
expect(loadPackage('@angular/common').esm5).toEqual('./esm5/common.js');
|
|
expect((loadPackage('@angular/common') as any).esm5_ivy_ngcc)
|
|
.toEqual('__ivy_ngcc__/esm5/common.js');
|
|
|
|
// Doesn't touch original files
|
|
expect(fs.readFile(_(`/node_modules/@angular/common/esm5/src/common_module.js`)))
|
|
.not.toMatch(ANGULAR_CORE_IMPORT_REGEX);
|
|
// Or create a backup of the original
|
|
expect(
|
|
fs.exists(_(`/node_modules/@angular/common/esm5/src/common_module.js.__ivy_ngcc_bak`)))
|
|
.toBe(false);
|
|
|
|
// Creates new files
|
|
expect(
|
|
fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/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__/esm5/src/version.js`)));
|
|
expect(fs.readFile(_(`/node_modules/@angular/common/__ivy_ngcc__/esm5/src/version.js`)))
|
|
.toEqual(fs.readFile(_(`/node_modules/@angular/common/esm5/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', 'fesm5'],
|
|
});
|
|
|
|
const pkg: any = loadPackage('@angular/core');
|
|
|
|
// `es2015` is an alias of `fesm2015`.
|
|
expect(pkg.fesm2015).toEqual('./fesm2015/core.js');
|
|
expect(pkg.es2015).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');
|
|
|
|
// `module` is an alias of `fesm5`.
|
|
expect(pkg.fesm5).toEqual('./fesm5/core.js');
|
|
expect(pkg.module).toEqual('./fesm5/core.js');
|
|
expect(pkg.fesm5_ivy_ngcc).toEqual('__ivy_ngcc__/fesm5/core.js');
|
|
expect(pkg.module_ivy_ngcc).toEqual('__ivy_ngcc__/fesm5/core.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, 'esm5_ivy_ngcc');
|
|
expectNotToHaveProp(pkg, 'fesm2015_ivy_ngcc');
|
|
expectNotToHaveProp(pkg, 'fesm5_ivy_ngcc');
|
|
expectNotToHaveProp(pkg, '__processed_by_ivy_ngcc__');
|
|
|
|
// Process `fesm2015` and update `package.json`.
|
|
pkg = processFormatAndUpdatePackageJson('fesm2015');
|
|
expectNotToHaveProp(pkg, 'esm5_ivy_ngcc');
|
|
expectToHaveProp(pkg, 'fesm2015_ivy_ngcc');
|
|
expectNotToHaveProp(pkg, 'fesm5_ivy_ngcc');
|
|
expectToHaveProp(pkg.__processed_by_ivy_ngcc__ !, 'fesm2015');
|
|
|
|
// Process `fesm5` and update `package.json`.
|
|
pkg = processFormatAndUpdatePackageJson('fesm5');
|
|
expectNotToHaveProp(pkg, 'esm5_ivy_ngcc');
|
|
expectToHaveProp(pkg, 'fesm2015_ivy_ngcc');
|
|
expectToHaveProp(pkg, 'fesm5_ivy_ngcc');
|
|
expectToHaveProp(pkg.__processed_by_ivy_ngcc__ !, 'fesm5');
|
|
|
|
// Process `esm5` and update `package.json`.
|
|
pkg = processFormatAndUpdatePackageJson('esm5');
|
|
expectToHaveProp(pkg, 'esm5_ivy_ngcc');
|
|
expectToHaveProp(pkg, 'fesm2015_ivy_ngcc');
|
|
expectToHaveProp(pkg, 'fesm5_ivy_ngcc');
|
|
expectToHaveProp(pkg.__processed_by_ivy_ngcc__ !, 'esm5');
|
|
|
|
// Ensure the properties are in deterministic order (regardless of processing order).
|
|
const pkgKeys = stringifyKeys(pkg);
|
|
expect(pkgKeys).toContain('|esm5_ivy_ngcc|esm5|');
|
|
expect(pkgKeys).toContain('|fesm2015_ivy_ngcc|fesm2015|');
|
|
expect(pkgKeys).toContain('|fesm5_ivy_ngcc|fesm5|');
|
|
|
|
// 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` <=> `es2015`
|
|
// - `fesm5` <=> `module`
|
|
expect(stringifyKeys(pkg.__processed_by_ivy_ngcc__ !))
|
|
.toBe('|es2015|esm5|fesm2015|fesm5|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: ['esm5'], logger: new MockLogger()});
|
|
// Check that common/testings ES5 was processed
|
|
let commonTesting =
|
|
JSON.parse(fs.readFile(_('/node_modules/@angular/common/testing/package.json')));
|
|
expect(hasBeenProcessed(commonTesting, 'esm5')).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] !== _('/node_modules/@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, 'esm5')).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([
|
|
_('/node_modules/@angular/common'), _('/node_modules/@angular/common/testing')
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('diagnostics', () => {
|
|
it('should fail with formatted diagnostics when an error diagnostic is produced', () => {
|
|
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');
|
|
}
|
|
});
|
|
});
|
|
|
|
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 find and compile packages accessible via the pathMappings', () => {
|
|
mainNgcc({
|
|
basePath: '/node_modules',
|
|
propertiesToConsider: ['es2015'],
|
|
pathMappings: {paths: {'*': ['dist/*']}, baseUrl: '/'},
|
|
});
|
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
|
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',
|
|
});
|
|
});
|
|
});
|
|
|
|
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({
|
|
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}
|
|
},
|
|
}
|
|
}};`,
|
|
},
|
|
]);
|
|
mainNgcc({basePath: '/node_modules', propertiesToConsider: ['es2015']});
|
|
// We process core but not core/testing.
|
|
expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toEqual({
|
|
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({
|
|
es2015: '0.0.0-PLACEHOLDER',
|
|
fesm2015: '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',
|
|
() => {
|
|
compileIntoFlatEs5Package('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: ['module'],
|
|
});
|
|
|
|
|
|
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<DerivedDir, "[base]", ["base1", "base2"], {}, {}, never>;');
|
|
});
|
|
|
|
it('should generate a component definition with CopyDefinitionFeature for an undecorated child component',
|
|
() => {
|
|
compileIntoFlatEs5Package('test-package', {
|
|
'/index.ts': `
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: '[base]',
|
|
template: '<span>This is the base template</span>',
|
|
})
|
|
export class BaseCmp {}
|
|
|
|
export class DerivedCmp extends BaseCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [DerivedCmp],
|
|
})
|
|
export class Module {}
|
|
`,
|
|
});
|
|
|
|
mainNgcc({
|
|
basePath: '/node_modules',
|
|
targetEntryPointPath: 'test-package',
|
|
propertiesToConsider: ['module'],
|
|
});
|
|
|
|
|
|
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<DerivedCmp, "[base]", never, {}, {}, never>;');
|
|
});
|
|
|
|
it('should generate directive definitions with CopyDefinitionFeature for undecorated child directives in a long inheritance chain',
|
|
() => {
|
|
compileIntoFlatEs5Package('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: ['module'],
|
|
});
|
|
|
|
const dtsContents = fs.readFile(_(`/node_modules/test-package/index.d.ts`));
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta<DerivedDir1, "[base]", never, {}, {}, never>;');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta<DerivedDir2, "[base]", never, {}, {}, never>;');
|
|
expect(dtsContents)
|
|
.toContain(
|
|
'static ɵdir: ɵngcc0.ɵɵDirectiveDefWithMeta<DerivedDir3, "[base]", never, {}, {}, never>;');
|
|
});
|
|
});
|
|
|
|
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: '<div i18n>Some message</div>'
|
|
})
|
|
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: '<div i18n>Some message</div>'
|
|
})
|
|
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;'
|
|
},
|
|
]);
|
|
|
|
// An Angular package that has 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: '<h2>Hello</h2>'}] }\n];`
|
|
},
|
|
{
|
|
name: _('/dist/local-package/index.d.ts'),
|
|
contents: `export declare class AppComponent {};`
|
|
},
|
|
]);
|
|
|
|
// 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: '<h2>Hello</h2>'}] }
|
|
];
|
|
`
|
|
},
|
|
{
|
|
name: _('/node_modules/invalid-package/index.d.ts'),
|
|
contents: `export declare class AppComponent {}`
|
|
},
|
|
{name: _('/node_modules/invalid-package/index.metadata.json'), contents: 'DUMMY DATA'},
|
|
]);
|
|
}
|
|
});
|
|
});
|
|
|
|
function countOccurrences(haystack: string, needle: string): number {
|
|
const matches = haystack.match(new RegExp(needle, 'g'));
|
|
return matches !== null ? matches.length : 0;
|
|
}
|