feat(ivy): ngcc - render private declaration exports (#26906)

Ngcc will now render additional exports for classes that are referenced in
`NgModule` decorated classes, but which were not publicly exported
from an entry-point of the package.

This is important because when ngtsc compiles libraries processed by ngcc
it needs to be able to publcly access decorated classes that are referenced
by `NgModule` decorated classes in order to build templates that use these
classes.

Doing this re-exporting is not without its risks. There are chances that
the class is not exported correctly: there may already be similarly named
exports from the entry-point or the class may be being aliased. But there
is not much more we can do from the point of view of ngcc to workaround
such scenarios. Generally, packages should have been built so that this
approach works.

PR Close #26906
This commit is contained in:
Pete Bacon Darwin
2018-11-25 22:07:51 +00:00
committed by Igor Minar
parent bf3ac41e36
commit 912b0529c1
6 changed files with 165 additions and 44 deletions

View File

@ -6,15 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {dirname} from 'canonical-path';
import * as ts from 'typescript';
import MagicString from 'magic-string';
import {makeTestEntryPointBundle} from '../helpers/utils';
import * as ts from 'typescript';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {EsmRenderer} from '../../src/rendering/esm_renderer';
import {makeTestEntryPointBundle} from '../helpers/utils';
function setup(file: {name: string, contents: string}) {
const dir = dirname(file.name);
@ -122,6 +121,24 @@ import * as i1 from '@angular/common';
});
});
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
{from: '/some/a.js', identifier: 'ComponentA1'},
{from: '/some/a.js', identifier: 'ComponentA2'},
{from: '/some/foo/b.js', identifier: 'ComponentB'},
{from: PROGRAM.name, identifier: 'TopLevelComponent'},
]);
expect(output.toString()).toContain(`
// Some other content
export {ComponentA1} from './a';
export {ComponentA2} from './a';
export {ComponentB} from './foo/b';
export {TopLevelComponent};`);
});
});
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {

View File

@ -8,13 +8,12 @@
import {dirname} from 'canonical-path';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {Esm5Renderer} from '../../src/rendering/esm5_renderer';
import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils';
function setup(file: {name: string, contents: string}) {
const dir = dirname(file.name);
@ -159,6 +158,24 @@ import * as i1 from '@angular/common';
});
});
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
{from: '/some/a.js', identifier: 'ComponentA1'},
{from: '/some/a.js', identifier: 'ComponentA2'},
{from: '/some/foo/b.js', identifier: 'ComponentB'},
{from: PROGRAM.name, identifier: 'TopLevelComponent'},
]);
expect(output.toString()).toContain(`
export {A, B, C, NoIife, BadIife};
export {ComponentA1} from './a';
export {ComponentA2} from './a';
export {ComponentB} from './foo/b';
export {TopLevelComponent};`);
});
});
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {

View File

@ -11,10 +11,10 @@ import * as ts from 'typescript';
import {fromObject, generateMapFileComment} from 'convert-source-map';
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Renderer} from '../../src/rendering/renderer';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {makeTestEntryPointBundle} from '../helpers/utils';
@ -25,6 +25,12 @@ class TestRenderer extends Renderer {
addImports(output: MagicString, imports: {name: string, as: string}[]) {
output.prepend('\n// ADD IMPORTS\n');
}
addExports(output: MagicString, baseEntryPointPath: string, exports: {
identifier: string,
from: string
}[]) {
output.prepend('\n// ADD EXPORTS\n');
}
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
output.prepend('\n// ADD CONSTANTS\n');
}
@ -51,12 +57,14 @@ function createTestRenderer(
new DecorationAnalyzer(typeChecker, host, referencesRegistry, bundle.rootDirs, isCore)
.analyzeProgram(bundle.src.program);
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyses =
new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
const renderer = new TestRenderer(host, isCore, bundle);
spyOn(renderer, 'addImports').and.callThrough();
spyOn(renderer, 'addDefinitions').and.callThrough();
spyOn(renderer, 'removeDecorators').and.callThrough();
return {renderer, decorationAnalyses, switchMarkerAnalyses};
return {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses};
}
@ -83,7 +91,7 @@ describe('Renderer', () => {
});
const RENDERED_CONTENTS =
`\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` +
`\n// ADD EXPORTS\n\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` +
INPUT_PROGRAM.contents;
const OUTPUT_PROGRAM_MAP = fromObject({
@ -92,14 +100,14 @@ describe('Renderer', () => {
'sources': ['/src/file.js'],
'sourcesContent': [INPUT_PROGRAM.contents],
'names': [],
'mappings': ';;;;;;;;AAAA;;;;;;;;;'
'mappings': ';;;;;;;;;;AAAA;;;;;;;;;'
});
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3,
'sources': ['/src/file.ts'],
'names': [],
'mappings': ';;;;;;;;AAAA',
'mappings': ';;;;;;;;;;AAAA',
'file': '/dist/file.js',
'sourcesContent': [INPUT_PROGRAM.contents]
});
@ -107,9 +115,10 @@ describe('Renderer', () => {
describe('renderProgram()', () => {
it('should render the modified contents; and a new map file, if the original provided no map file.',
() => {
const {renderer, decorationAnalyses, switchMarkerAnalyses} =
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
@ -120,9 +129,10 @@ describe('Renderer', () => {
describe('calling abstract methods', () => {
it('should call addImports with the source code and info about the core Angular library.',
() => {
const {decorationAnalyses, renderer, switchMarkerAnalyses} =
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addImportsSpy.calls.first().args[1]).toEqual([
@ -132,9 +142,10 @@ describe('Renderer', () => {
it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.',
() => {
const {decorationAnalyses, renderer, switchMarkerAnalyses} =
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({
@ -151,9 +162,10 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
() => {
const {decorationAnalyses, renderer, switchMarkerAnalyses} =
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy;
expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
@ -175,12 +187,14 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
describe('source map merging', () => {
it('should merge any inline source map from the original file and write the output as an inline source map',
() => {
const {decorationAnalyses, renderer, switchMarkerAnalyses} = createTestRenderer(
'test-package', [{
...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
}]);
const result = renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer(
'test-package', [{
...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
}]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment());
@ -191,12 +205,14 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
() => {
// Mock out reading the map file from disk
spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON());
const {decorationAnalyses, renderer, switchMarkerAnalyses} = createTestRenderer(
'test-package', [{
...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}]);
const result = renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer(
'test-package', [{
...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
@ -250,9 +266,10 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
describe('rendering typings', () => {
it('should render extract types into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses} =
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM);
const result = renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents)
@ -261,13 +278,30 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
});
it('should render imports into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses} =
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM);
const result = renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`);
});
it('should render exports into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM);
// Add a mock export to trigger export rendering
privateDeclarationsAnalyses.push(
{identifier: 'ComponentB', from: '/src/file.js', dtsFrom: '/typings/b.d.ts'});
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents)
.toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`);
});
});
});
});