fix(ngcc): do not collect private declarations from external packages (#34811)

Previously, while trying to build an `NgccReflectionHost`'s
`privateDtsDeclarationMap`, `computePrivateDtsDeclarationMap()` would
try to collect exported declarations from all source files of the
program (i.e. without checking whether they were within the target
package, as happens for declarations in `.d.ts` files).

Most of the time, that would not be a problem, because external packages
would be represented as `.d.ts` files in the program. But when an
external package had no typings, the JS files would be used instead. As
a result, the `ReflectionHost` would try to (unnecessarilly) parse the
file in order to extract exported declarations, which in turn would be
harmless in most cases.

There are certain cases, though, where the `ReflectionHost` would throw
an error, because it cannot parse the external package's JS file. This
could happen, for example, in `UmdReflectionHost`, which expects the
file to contain exactly one statement. See #34544 for more details on a
real-world failure.

This commit fixes the issue by ensuring that
`computePrivateDtsDeclarationMap()` will only collect exported
declarations from files within the target package.

Jira issue: [FW-1794](https://angular-team.atlassian.net/browse/FW-1794)

Fixes #34544

PR Close #34811
This commit is contained in:
George Kalpakas 2020-01-15 21:54:36 +02:00 committed by Andrew Kushnir
parent ba2bf82540
commit 5b42084912
10 changed files with 76 additions and 16 deletions

View File

@ -15,7 +15,7 @@ node $(pwd)/../../scripts/build-packages-dist.js
# Workaround https://github.com/yarnpkg/yarn/issues/2165 # Workaround https://github.com/yarnpkg/yarn/issues/2165
# Yarn will cache file://dist URIs and not update Angular code # Yarn will cache file://dist URIs and not update Angular code
readonly cache=../.yarn_local_cache export readonly cache=../.yarn_local_cache
function rm_cache { function rm_cache {
rm -rf $cache rm -rf $cache
} }

View File

@ -13,6 +13,7 @@
"@angular/material": "9.0.0-rc.4", "@angular/material": "9.0.0-rc.4",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic", "@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/router": "file:../../dist/packages-dist/router", "@angular/router": "file:../../dist/packages-dist/router",
"@types/node": "file:../../node_modules/@types/node", "@types/node": "file:../../node_modules/@types/node",
"rxjs": "file:../../node_modules/rxjs", "rxjs": "file:../../node_modules/rxjs",

View File

@ -140,6 +140,17 @@ assertSucceeded "Expected 'ngcc' to log 'Compiling'."
assertSucceeded "Expected 'ngcc' to not add trailing commas to factory function parameters in UMD." assertSucceeded "Expected 'ngcc' to not add trailing commas to factory function parameters in UMD."
# Can it compile `@angular/platform-server` in UMD + typings without errors?
# (The CLI prefers the `main` property (which maps to UMD) over `module` when compiling `@angular/platform-server`.
# See https://github.com/angular/angular-cli/blob/e36853338/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/server.ts#L34)
rm -rf node_modules/@angular/platform-server && \
yarn install --cache-folder $cache --check-files && \
test -d node_modules/@angular/platform-server
assertSucceeded "Expected to re-install '@angular/platform-server'."
ngcc --properties main --target @angular/platform-server
assertSucceeded "Expected 'ngcc' to successfully compile '@angular/platform-server' (main)."
# Can it be safely run again (as a noop)? # Can it be safely run again (as a noop)?
# And check that it logged skipping compilation as expected # And check that it logged skipping compilation as expected
ngcc -l debug | grep 'Skipping' ngcc -l debug | grep 'Skipping'

View File

@ -49,6 +49,12 @@
"@angular/platform-browser@file:../../dist/packages-dist/platform-browser": "@angular/platform-browser@file:../../dist/packages-dist/platform-browser":
version "9.0.0-rc.1" version "9.0.0-rc.1"
"@angular/platform-server@file:../../dist/packages-dist/platform-server":
version "9.0.0-rc.1"
dependencies:
domino "^2.1.2"
xhr2 "^0.1.4"
"@angular/router@file:../../dist/packages-dist/router": "@angular/router@file:../../dist/packages-dist/router":
version "9.0.0-rc.1" version "9.0.0-rc.1"
@ -853,6 +859,11 @@ dev-ip@^1.0.1:
resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0"
integrity sha1-p2o+0YVb56ASu4rBbLgPPADcKPA= integrity sha1-p2o+0YVb56ASu4rBbLgPPADcKPA=
domino@^2.1.2:
version "2.1.4"
resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.4.tgz#78922e7fab7c610f35792b6c745b7962d342e9c4"
integrity sha512-l70mlQ7IjPKC8kT7GljQXJZmt5OqFL+RE91ik5y5WWQtsd9wP8R7gpFnNu96fK5MqAAZRXfLLsnzKtkty5fWGQ==
easy-extender@^2.3.4: easy-extender@^2.3.4:
version "2.3.4" version "2.3.4"
resolved "https://registry.yarnpkg.com/easy-extender/-/easy-extender-2.3.4.tgz#298789b64f9aaba62169c77a2b3b64b4c9589b8f" resolved "https://registry.yarnpkg.com/easy-extender/-/easy-extender-2.3.4.tgz#298789b64f9aaba62169c77a2b3b64b4c9589b8f"
@ -3479,6 +3490,11 @@ ws@~3.3.1:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
ultron "~1.1.0" ultron "~1.1.0"
xhr2@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8=
xml2js@^0.4.17: xml2js@^0.4.17:
version "0.4.19" version "0.4.19"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"

View File

@ -38,7 +38,7 @@ fi
# Workaround https://github.com/yarnpkg/yarn/issues/2165 # Workaround https://github.com/yarnpkg/yarn/issues/2165
# Yarn will cache file://dist URIs and not update Angular code # Yarn will cache file://dist URIs and not update Angular code
readonly cache=.yarn_local_cache export readonly cache=.yarn_local_cache
function rm_cache { function rm_cache {
rm -rf $cache rm -rf $cache
} }

View File

@ -50,7 +50,7 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
*/ */
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
/** /**
* A mapping from source declarations typings declarations, which are both publicly exported. * A mapping from source declarations to typings declarations, which are both publicly exported.
* *
* There should be one entry for every public export visible from the root file of the source * There should be one entry for every public export visible from the root file of the source
* tree. Note that by definition the key and value declarations will not be in the same TS * tree. Note that by definition the key and value declarations will not be in the same TS
@ -1573,14 +1573,14 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
Map<ts.Declaration, ts.Declaration> { Map<ts.Declaration, ts.Declaration> {
const declarationMap = new Map<ts.Declaration, ts.Declaration>(); const declarationMap = new Map<ts.Declaration, ts.Declaration>();
const dtsDeclarationMap = new Map<string, ts.Declaration>(); const dtsDeclarationMap = new Map<string, ts.Declaration>();
const dtsFiles = getNonRootFiles(dts);
const typeChecker = dts.program.getTypeChecker(); const typeChecker = dts.program.getTypeChecker();
const dtsFiles = getNonRootPackageFiles(dts);
for (const dtsFile of dtsFiles) { for (const dtsFile of dtsFiles) {
if (isWithinPackage(dts.package, dtsFile)) {
this.collectDtsExportedDeclarations(dtsDeclarationMap, dtsFile, typeChecker); this.collectDtsExportedDeclarations(dtsDeclarationMap, dtsFile, typeChecker);
} }
}
const srcFiles = getNonRootFiles(src); const srcFiles = getNonRootPackageFiles(src);
for (const srcFile of srcFiles) { for (const srcFile of srcFiles) {
this.collectSrcExportedDeclarations(declarationMap, dtsDeclarationMap, srcFile); this.collectSrcExportedDeclarations(declarationMap, dtsDeclarationMap, srcFile);
} }
@ -2070,7 +2070,8 @@ function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
return rootFile; return rootFile;
} }
function getNonRootFiles(bundle: BundleProgram): ts.SourceFile[] { function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
const rootFile = bundle.program.getSourceFile(bundle.path); const rootFile = bundle.program.getSourceFile(bundle.path);
return bundle.program.getSourceFiles().filter(f => f !== rootFile); return bundle.program.getSourceFiles().filter(
f => (f !== rootFile) && isWithinPackage(bundle.package, f));
} }

View File

@ -2184,7 +2184,7 @@ exports.ExternalModule = ExternalModule;
}); });
}); });
describe('getDtsDeclarationsOfClass()', () => { describe('getDtsDeclaration()', () => {
it('should find the dts declaration that has the same relative path to the source file', it('should find the dts declaration that has the same relative path to the source file',
() => { () => {
loadTestFiles(TYPINGS_SRC_FILES); loadTestFiles(TYPINGS_SRC_FILES);

View File

@ -547,6 +547,7 @@ runInEachFileSystem(() => {
name: _('/ep/src/index.js'), name: _('/ep/src/index.js'),
contents: ` contents: `
import 'an_external_lib'; import 'an_external_lib';
import 'an_external_lib_without_typings';
import {InternalClass} from './internal'; import {InternalClass} from './internal';
import * as func1 from './func1'; import * as func1 from './func1';
import * as missing from './missing-class'; import * as missing from './missing-class';
@ -579,6 +580,10 @@ runInEachFileSystem(() => {
name: _('/ep/src/shadow-class.js'), name: _('/ep/src/shadow-class.js'),
contents: 'export class ShadowClass {}', contents: 'export class ShadowClass {}',
}, },
{
name: _('/an_external_lib_without_typings/index.js'),
contents: '// Some content.',
},
]; ];
TYPINGS_DTS_FILES = [ TYPINGS_DTS_FILES = [
@ -1958,7 +1963,7 @@ runInEachFileSystem(() => {
}); });
}); });
describe('getDtsDeclarationsOfClass()', () => { describe('getDtsDeclaration()', () => {
it('should find the dts declaration that has the same relative path to the source file', it('should find the dts declaration that has the same relative path to the source file',
() => { () => {
loadTestFiles(TYPINGS_SRC_FILES); loadTestFiles(TYPINGS_SRC_FILES);
@ -2019,15 +2024,41 @@ runInEachFileSystem(() => {
getRootFiles(TYPINGS_SRC_FILES)[0], false, [_('/ep/src/shadow-class.js')]); getRootFiles(TYPINGS_SRC_FILES)[0], false, [_('/ep/src/shadow-class.js')]);
const dts = makeTestBundleProgram( const dts = makeTestBundleProgram(
getRootFiles(TYPINGS_DTS_FILES)[0], false, [_('/ep/typings/shadow-class.d.ts')]); getRootFiles(TYPINGS_DTS_FILES)[0], false, [_('/ep/typings/shadow-class.d.ts')]);
const missingClass = getDeclaration( const shadowClass = getDeclaration(
bundle.program, _('/ep/src/shadow-class.js'), 'ShadowClass', isNamedClassDeclaration); bundle.program, _('/ep/src/shadow-class.js'), 'ShadowClass', isNamedClassDeclaration);
const host = new Esm2015ReflectionHost(new MockLogger(), false, bundle, dts); const host = new Esm2015ReflectionHost(new MockLogger(), false, bundle, dts);
const dtsDecl = host.getDtsDeclaration(missingClass) !; const dtsDecl = host.getDtsDeclaration(shadowClass) !;
expect(dtsDecl).not.toBeNull(); expect(dtsDecl).not.toBeNull();
expect(dtsDecl.getSourceFile().fileName).toEqual(_('/ep/typings/shadow-class.d.ts')); expect(dtsDecl.getSourceFile().fileName).toEqual(_('/ep/typings/shadow-class.d.ts'));
}); });
it('should ignore source files outside of the entrypoint', () => {
const externalLibWithoutTypingsIndex = _('/an_external_lib_without_typings/index.js');
class TestEsm2015ReflectionHost extends Esm2015ReflectionHost {
getExportsOfModule(node: ts.Node) {
if (ts.isSourceFile(node) && (node.fileName === externalLibWithoutTypingsIndex)) {
throw new Error(
`'getExportsOfModule()' called on '${externalLibWithoutTypingsIndex}'.`);
}
return super.getExportsOfModule(node);
}
}
loadTestFiles(TYPINGS_SRC_FILES);
loadTestFiles(TYPINGS_DTS_FILES);
const bundle = makeTestBundleProgram(
getRootFiles(TYPINGS_SRC_FILES)[0], false, [externalLibWithoutTypingsIndex]);
const dts = makeTestBundleProgram(getRootFiles(TYPINGS_DTS_FILES)[0]);
const missingClass = getDeclaration(
bundle.program, _('/ep/src/missing-class.js'), 'MissingClass2',
isNamedClassDeclaration);
const host = new TestEsm2015ReflectionHost(new MockLogger(), false, bundle, dts);
expect(host.getDtsDeclaration(missingClass)).toBeNull();
});
it('should find the dts file that contains a matching class declaration, even if the source files do not match', it('should find the dts file that contains a matching class declaration, even if the source files do not match',
() => { () => {
loadTestFiles(TYPINGS_SRC_FILES); loadTestFiles(TYPINGS_SRC_FILES);

View File

@ -2450,7 +2450,7 @@ runInEachFileSystem(() => {
}); });
}); });
describe('getDtsDeclarationsOfClass()', () => { describe('getDtsDeclaration()', () => {
it('should find the dts declaration that has the same relative path to the source file', it('should find the dts declaration that has the same relative path to the source file',
() => { () => {
loadTestFiles(TYPINGS_SRC_FILES); loadTestFiles(TYPINGS_SRC_FILES);

View File

@ -2360,7 +2360,7 @@ runInEachFileSystem(() => {
}); });
}); });
describe('getDtsDeclarationsOfClass()', () => { describe('getDtsDeclaration()', () => {
it('should find the dts declaration that has the same relative path to the source file', it('should find the dts declaration that has the same relative path to the source file',
() => { () => {
loadTestFiles(TYPINGS_SRC_FILES); loadTestFiles(TYPINGS_SRC_FILES);