fix(ivy): disable use of aliasing in template type-checking (#34649)

FileToModuleHost aliasing supports compilation within environments that have
two properties:

1. A `FileToModuleHost` exists which defines canonical module names for any
   given TS file.
2. Dependency restrictions exist which prevent the import of arbitrary files
   even if such files are within the .d.ts transitive closure of a
   compilation ("strictdeps").

In such an environment, generated imports can only go through import paths
which are already present in the user program. The aliasing system supports
the generation and consumption of such imports at runtime.

`FileToModuleHost` aliasing does not emit re-exports in .d.ts files. This
means that it's safe to rely on alias re-exports in generated .js code (they
are guaranteed to exist at runtime) but not in template type-checking code
(since TS will not be able to follow such imports). Therefore, non-aliased
imports should be used in template type-checking code.

This commit adds a `NoAliasing` flag to `ImportFlags` and sets it when
generating imports in template type-checking code. The testing environment
is also patched to support resolution of FileToModuleHost canonical paths
within the template type-checking program, enabling testing of this change.

PR Close #34649
This commit is contained in:
Alex Rickabaugh
2019-12-19 16:33:56 -08:00
committed by Andrew Kushnir
parent 5b9c96b9b8
commit cb11380515
6 changed files with 109 additions and 3 deletions

View File

@ -248,12 +248,31 @@ class AugmentedCompilerHost extends NgtscCompilerHost {
delegate !: ts.CompilerHost;
}
const ROOT_PREFIX = 'root/';
class FileNameToModuleNameHost extends AugmentedCompilerHost {
fileNameToModuleName(importedFilePath: string): string {
const relativeFilePath = this.fs.relative(this.fs.pwd(), this.fs.resolve(importedFilePath));
const rootedPath = this.fs.join('root', relativeFilePath);
return rootedPath.replace(/(\.d)?.ts$/, '');
}
resolveModuleNames(
moduleNames: string[], containingFile: string, reusedNames: string[]|undefined,
redirectedReference: ts.ResolvedProjectReference|undefined,
options: ts.CompilerOptions): (ts.ResolvedModule|undefined)[] {
return moduleNames.map(moduleName => {
if (moduleName.startsWith(ROOT_PREFIX)) {
// Strip the artificially added root prefix.
moduleName = '/' + moduleName.substr(ROOT_PREFIX.length);
}
return ts
.resolveModuleName(
moduleName, containingFile, options, this, /* cache */ undefined, redirectedReference)
.resolvedModule;
});
}
}
class MultiCompileHostExt extends AugmentedCompilerHost implements Partial<ts.CompilerHost> {

View File

@ -1173,6 +1173,71 @@ export declare class AnimationEvent {
expect(getSourceCodeForDiagnostic(diags[0])).toEqual('y = !y');
});
it('should still type-check when fileToModuleName aliasing is enabled, but alias exports are not in the .d.ts file',
() => {
// The template type-checking file imports directives/pipes in order to type-check their
// usage. When `FileToModuleHost` aliasing is enabled, these imports would ordinarily use
// aliased values. However, such aliases are not guaranteed to exist in the .d.ts files,
// and so feeding such imports back into TypeScript does not work.
//
// Instead, direct imports should be used within template type-checking code. This test
// verifies that template type-checking is able to cope with such a scenario where
// aliasing is enabled and alias re-exports don't exist in .d.ts files.
env.tsconfig({
// Setting this private flag turns on aliasing.
'_useHostForImportGeneration': true,
// Because the tsconfig is overridden, template type-checking needs to be turned back on
// explicitly as well.
'fullTemplateTypeCheck': true,
});
// 'alpha' declares the directive which will ultimately be imported.
env.write('alpha.d.ts', `
import {ɵɵDirectiveDefWithMeta, ɵɵNgModuleDefWithMeta} from '@angular/core';
export declare class ExternalDir {
input: string;
static ɵdir: ɵɵDirectiveDefWithMeta<ExternalDir, '[test]', never, { 'input': "input" }, never, never>;
}
export declare class AlphaModule {
static ɵmod: ɵɵNgModuleDefWithMeta<AlphaModule, [typeof ExternalDir], never, [typeof ExternalDir]>;
}
`);
// 'beta' re-exports AlphaModule from alpha.
env.write('beta.d.ts', `
import {ɵɵNgModuleDefWithMeta} from '@angular/core';
import {AlphaModule} from './alpha';
export declare class BetaModule {
static ɵmod: ɵɵNgModuleDefWithMeta<BetaModule, never, never, [typeof AlphaModule]>;
}
`);
// The application imports BetaModule from beta, gaining visibility of ExternalDir from
// alpha.
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
import {BetaModule} from './beta';
@Component({
selector: 'cmp',
template: '<div test input="value"></div>',
})
export class Cmp {}
@NgModule({
declarations: [Cmp],
imports: [BetaModule],
})
export class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
describe('input coercion', () => {
beforeEach(() => {
env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true});