fix(ivy): emit fs-relative paths when rootDir(s) aren't in effect (#33828)

Previously, the compiler assumed that all TS files logically within a
project existed under one or more "root directories". If the TS compiler
option `rootDir` or `rootDirs` was set, they would dictate the root
directories in use, otherwise the current directory was used.

Unfortunately this assumption was unfounded - it's common for projects
without explicit `rootDirs` to import from files outside the current
working directory. In such cases the `LogicalProjectStrategy` would attempt
to generate imports into those files, and fail. This would lead to no
`ReferenceEmitStrategy` being able to generate an import, and end in a
compiler assertion failure.

This commit introduces a new strategy to use when there are no `rootDirs`
explicitly present, the `RelativePathStrategy`. It uses simpler, filesystem-
relative paths to generate imports, even to files above the current working
directory.

Fixes #33659
Fixes #33562

PR Close #33828
This commit is contained in:
Alex Rickabaugh
2019-11-13 10:52:49 -08:00
parent 51720745dd
commit 850aee2448
5 changed files with 181 additions and 11 deletions

View File

@ -68,7 +68,17 @@ This `ReferenceEmitStrategy` queries the `Reference` for a `ts.Identifier` that'
### `LogicalProjectStrategy`
This `ReferenceEmitStrategy` is used to import referenced classes that are declared in the current project, and not in any third-party or external libraries. It constructs an import path that's valid within the logical filesystem of the project, even if the project has multiple `rootDirs`.
This `ReferenceEmitStrategy` is used to import referenced classes that are declared in the current project, and not in any third-party or external libraries, whenever `rootDir` or `rootDirs` is set in the TS compiler options.
When `rootDir`(s) are present, multiple physical directories can be mapped into the same logical namespace. So consider two files `/app/app.cmp.ts` and `/lib/lib.cmp.ts`. Ordinarily, a relative import (such as the kind generated by `RelativePathStrategy`) from the former to the latter would be `../lib/lib.cmp`. However, if both `/app` and `/lib` are project `rootDirs`, then the files within are logically in the same "directory", and the correct import is `./lib.cmp`.
The `LogicalProjectStrategy` constructs `LogicalProjectPath`s between files and generates module specifiers that are relative imports within that namespace, honoring the project's `rootDirs` settings.
The `LogicalProjectStrategy` will decline to generate an import into any file which falls outside the project's `rootDirs`, as such a relative specifier is not representable in the merged namespace.
### `RelativePathStrategy`
This `ReferenceEmitStrategy` is used to generate relative imports between two files in the project, assuming the layout of files on the disk maps directly to the module specifier namespace. This is the case if the project does not have `rootDir`/`rootDirs` configured in its TS compiler options.
### `AbsoluteModuleStrategy`

View File

@ -9,7 +9,7 @@
export {AliasStrategy, AliasingHost, FileToModuleAliasingHost, PrivateExportAliasingHost} from './src/alias';
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default';
export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter} from './src/emitter';
export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy} from './src/emitter';
export {Reexport} from './src/reexport';
export {ImportMode, OwningModule, Reference} from './src/references';
export {ModuleResolver} from './src/resolver';

View File

@ -7,12 +7,17 @@
*/
import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {LogicalFileSystem, LogicalProjectPath, absoluteFrom} from '../../file_system';
import {LogicalFileSystem, LogicalProjectPath, PathSegment, absoluteFrom, absoluteFromSourceFile, basename, dirname, relative, resolve} from '../../file_system';
import {stripExtension} from '../../file_system/src/util';
import {ReflectionHost} from '../../reflection';
import {getSourceFile, getSourceFileOrNull, isDeclaration, nodeNameForError, resolveModuleName} from '../../util/src/typescript';
import {findExportedNameOfNode} from './find_export';
import {ImportMode, Reference} from './references';
/**
* A host which supports an operation to convert a file name into a module name.
*
@ -233,6 +238,28 @@ export class LogicalProjectStrategy implements ReferenceEmitStrategy {
}
}
/**
* A `ReferenceEmitStrategy` which constructs relatives paths between `ts.SourceFile`s.
*
* This strategy can be used if there is no `rootDir`/`rootDirs` structure for the project which
* necessitates the stronger logic of `LogicalProjectStrategy`.
*/
export class RelativePathStrategy implements ReferenceEmitStrategy {
constructor(private reflector: ReflectionHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null {
const destSf = getSourceFile(ref.node);
let moduleName = stripExtension(
relative(dirname(absoluteFromSourceFile(context)), absoluteFromSourceFile(destSf)));
if (!moduleName.startsWith('../')) {
moduleName = ('./' + moduleName) as PathSegment;
}
const name = findExportedNameOfNode(ref.node, destSf, this.reflector);
return new ExternalExpr({moduleName, name});
}
}
/**
* A `ReferenceEmitStrategy` which uses a `FileToModuleHost` to generate absolute import references.
*/