Skip to content

Commit

Permalink
fix(ngcc): always add exports for ModuleWithProviders references
Browse files Browse the repository at this point in the history
In #32902 a bug was supposedly fixed where internal classes as used
within `ModuleWithProviders` are publicly exported, even when the
typings file already contained the generic type on the
`ModuleWithProviders`. This fix turns out to have been incomplete, as
the `ModuleWithProviders` analysis is not done when not processing the
typings files.

The effect of this bug is that formats that are processed after the
initial format had been processed would not have exports for internal
symbols, resulting in "export '...' was not found in '...'" errors.

This commit fixes the bug by always running the `ModuleWithProviders`
analyzer. An integration test has been added that would fail prior to
this change.

Fixes #33701
  • Loading branch information
JoostK committed Nov 18, 2019
1 parent 6fb6add commit d1049b0
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 38 deletions.
Expand Up @@ -30,7 +30,9 @@ export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProviders
export const ModuleWithProvidersAnalyses = Map;

export class ModuleWithProvidersAnalyzer {
constructor(private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry) {}
constructor(
private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
private processDts: boolean) {}

analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
const analyses = new ModuleWithProvidersAnalyses();
Expand All @@ -43,16 +45,19 @@ export class ModuleWithProvidersAnalyzer {
this.referencesRegistry.add(fn.ngModule.node, new Reference(fn.ngModule.node));
}

const dtsFn = this.getDtsDeclarationForFunction(fn);
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
null;
if (!typeParam || isAnyKeyword(typeParam)) {
const ngModule = this.resolveNgModuleReference(fn);
const dtsFile = dtsFn.getSourceFile();
const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile) : [];
analysis.push({declaration: dtsFn, ngModule});
analyses.set(dtsFile, analysis);
// Only when processing the dts files do we need to determine which declaration to update.
if (this.processDts) {
const dtsFn = this.getDtsDeclarationForFunction(fn);
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
null;
if (!typeParam || isAnyKeyword(typeParam)) {
const ngModule = this.resolveNgModuleReference(fn);
const dtsFile = dtsFn.getSourceFile();
const analysis = analyses.has(dtsFile) ? analyses.get(dtsFile) : [];
analysis.push({declaration: dtsFn, ngModule});
analyses.set(dtsFile, analysis);
}
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/ngcc/src/packages/transformer.ts
Expand Up @@ -149,7 +149,7 @@ export class Transformer {
const decorationAnalyses = decorationAnalyzer.analyzeProgram();

const moduleWithProvidersAnalyzer =
bundle.dts && new ModuleWithProvidersAnalyzer(reflectionHost, referencesRegistry);
new ModuleWithProvidersAnalyzer(reflectionHost, referencesRegistry, bundle.dts !== null);
const moduleWithProvidersAnalyses = moduleWithProvidersAnalyzer &&
moduleWithProvidersAnalyzer.analyzeProgram(bundle.src.program);

Expand Down
Expand Up @@ -338,7 +338,8 @@ runInEachFileSystem(() => {
new MockLogger(), false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host);

const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
const processDts = true;
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry, processDts);
analyses = analyzer.analyzeProgram(program);
});

Expand Down Expand Up @@ -427,21 +428,19 @@ runInEachFileSystem(() => {

describe('tracking references when generic types already present', () => {
let _: typeof absoluteFrom;
let analyses: ModuleWithProvidersAnalyses;
let program: ts.Program;
let dtsProgram: BundleProgram;
let referencesRegistry: NgccReferencesRegistry;
let TEST_DTS_PROGRAM: TestFile[];
let TEST_PROGRAM: TestFile[];

beforeEach(() => {
_ = absoluteFrom;

const TEST_PROGRAM: TestFile[] = [
TEST_PROGRAM = [
{
name: _('/node_modules/test-package/src/entry-point.js'),
contents: `
export * from './explicit';
export * from './module';
`
`,
},
{
name: _('/node_modules/test-package/src/explicit.js'),
Expand Down Expand Up @@ -469,26 +468,26 @@ runInEachFileSystem(() => {
};
}
}
`
`,
},
{
name: _('/node_modules/test-package/src/module.js'),
contents: `
export class ExternalModule {}
`
`,
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
contents: 'export declare class LibraryModule {}',
},
];
const TEST_DTS_PROGRAM: TestFile[] = [
TEST_DTS_PROGRAM = [
{
name: _('/node_modules/test-package/typings/entry-point.d.ts'),
contents: `
export * from './explicit';
export * from './module';
`
`,
},
{
name: _('/node_modules/test-package/typings/explicit.d.ts'),
Expand All @@ -502,13 +501,13 @@ runInEachFileSystem(() => {
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
`,
},
{
name: _('/node_modules/test-package/typings/module.d.ts'),
contents: `
export declare class ExternalModule {}
`
`,
},
{
name: _('/node_modules/test-package/typings/core.d.ts'),
Expand All @@ -522,29 +521,31 @@ runInEachFileSystem(() => {
ngModule: Type<T>
providers?: Provider[]
}
`
`,
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
contents: 'export declare class LibraryModule {}',
},
];
loadTestFiles(TEST_PROGRAM);
loadTestFiles(TEST_DTS_PROGRAM);
});

it('should track references even when nothing needs to be updated', () => {
const bundle = makeTestEntryPointBundle(
'test-package', 'esm2015', false, getRootFiles(TEST_PROGRAM),
getRootFiles(TEST_DTS_PROGRAM));
program = bundle.src.program;
dtsProgram = bundle.dts !;
const program = bundle.src.program;
const dtsProgram = bundle.dts !;
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host);
const referencesRegistry = new NgccReferencesRegistry(host);

const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
analyses = analyzer.analyzeProgram(program);
});
const processDts = true;
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry, processDts);
const analyses = analyzer.analyzeProgram(program);

it('should track references even when nothing needs to be updated', () => {
const file = getSourceFileOrError(
dtsProgram.program, _('/node_modules/test-package/typings/explicit.d.ts'));
expect(analyses.has(file)).toBe(false);
Expand All @@ -563,5 +564,34 @@ runInEachFileSystem(() => {
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
});

it('should track references even when typings have already been processed', () => {
const bundle =
makeTestEntryPointBundle('test-package', 'esm2015', false, getRootFiles(TEST_PROGRAM));
const program = bundle.src.program;
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker(), null);
const referencesRegistry = new NgccReferencesRegistry(host);

const processDts = false; // Emulate the scenario where typings have already been processed
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry, processDts);
const analyses = analyzer.analyzeProgram(program);

expect(analyses.size).toBe(0);

const declarations = referencesRegistry.getDeclarationMap();
const explicitInternalModuleDeclaration = getDeclaration(
program, absoluteFrom('/node_modules/test-package/src/explicit.js'),
'ExplicitInternalModule', ts.isClassDeclaration);
const externalModuleDeclaration = getDeclaration(
program, absoluteFrom('/node_modules/test-package/src/module.js'), 'ExternalModule',
ts.isClassDeclaration);
const libraryModuleDeclaration = getDeclaration(
program, absoluteFrom('/node_modules/some-library/index.d.ts'), 'LibraryModule',
ts.isClassDeclaration);
expect(declarations.has(explicitInternalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
});
});
});
48 changes: 48 additions & 0 deletions packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
Expand Up @@ -175,6 +175,54 @@ runInEachFileSystem(() => {
expect(jsContents).toMatch(/\bvar _c0 =/);
});

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';`);
});

describe('in async mode', () => {
it('should run ngcc without errors for fesm2015', async() => {
const promise = mainNgcc({
Expand Down
Expand Up @@ -76,7 +76,8 @@ function createTestRenderer(
const decorationAnalyses =
new DecorationAnalyzer(fs, bundle, host, referencesRegistry).analyzeProgram();
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
new ModuleWithProvidersAnalyzer(host, referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyses =
new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
const testFormatter = new TestRenderingFormatter();
Expand Down
Expand Up @@ -574,7 +574,7 @@ export { D };

const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(host, referencesRegistry)
new ModuleWithProvidersAnalyzer(host, referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const typingsFile = getSourceFileOrError(bundle.dts !.program, _('/typings/index.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !;
Expand Down Expand Up @@ -610,7 +610,7 @@ export { D };

const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(host, referencesRegistry)
new ModuleWithProvidersAnalyzer(host, referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const typingsFile =
getSourceFileOrError(bundle.dts !.program, _('/typings/module.d.ts'));
Expand Down

0 comments on commit d1049b0

Please sign in to comment.