From 43226cb93db4f8bdf2ef2da375fe817d764a46a0 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 15 Aug 2017 17:06:09 -0700 Subject: [PATCH] feat(compiler): use typescript for resolving resource paths This can also be customized via the new method `resourceNameToFileName` in the `CompilerHost`. --- packages/compiler-cli/src/compiler_host.ts | 10 +++- .../src/diagnostics/typescript_symbols.ts | 2 +- packages/compiler-cli/src/transformers/api.ts | 7 ++- .../src/transformers/compiler_host.ts | 46 +++++++++++++------ .../compiler-cli/src/transformers/program.ts | 4 ++ .../test/transformers/compiler_host_spec.ts | 25 +++++++++- packages/compiler/src/aot/compiler_factory.ts | 19 +++++++- packages/compiler/src/aot/compiler_host.ts | 5 ++ packages/compiler/src/i18n/extractor.ts | 12 ++++- packages/compiler/src/url_resolver.ts | 19 +++----- packages/compiler/test/aot/test_util.ts | 30 +++++++++++- 11 files changed, 142 insertions(+), 37 deletions(-) diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 588bfd7779..63b7d595d4 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompilerHost, StaticSymbol, syntaxError} from '@angular/compiler'; +import {AotCompilerHost, StaticSymbol, UrlResolver, createOfflineCompileUrlResolver, syntaxError} from '@angular/compiler'; import {AngularCompilerOptions, CollectorOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped'; import * as fs from 'fs'; import * as path from 'path'; @@ -40,6 +40,8 @@ export abstract class BaseAotCompilerHost abstract moduleNameToFileName(m: string, containingFile: string): string|null; + abstract resourceNameToFileName(m: string, containingFile: string): string|null; + abstract fileNameToModuleName(importedFile: string, containingFile: string): string|null; abstract toSummaryFileName(fileName: string, referringSrcFileName: string): string; @@ -234,6 +236,7 @@ export class CompilerHost extends BaseAotCompilerHost { private isGenDirChildOfRootDir: boolean; private genDir: string; protected resolveModuleNameHost: CompilerHostContext; + private urlResolver: UrlResolver; constructor( program: ts.Program, options: AngularCompilerOptions, context: CompilerHostContext, @@ -266,6 +269,7 @@ export class CompilerHost extends BaseAotCompilerHost { } return false; }; + this.urlResolver = createOfflineCompileUrlResolver(); } toSummaryFileName(fileName: string, referringSrcFileName: string): string { @@ -382,6 +386,10 @@ export class CompilerHost extends BaseAotCompilerHost { } } + resourceNameToFileName(m: string, containingFile: string): string { + return this.urlResolver.resolve(containingFile, m); + } + /** * Moves the path into `genDir` folder while preserving the `node_modules` directory. */ diff --git a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts index 9d5199be2b..01371b42cc 100644 --- a/packages/compiler-cli/src/diagnostics/typescript_symbols.ts +++ b/packages/compiler-cli/src/diagnostics/typescript_symbols.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler'; +import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, extractProgramSymbols} from '@angular/compiler'; import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import * as fs from 'fs'; import * as path from 'path'; diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index fb2a9e4965..44fcd319f0 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -126,10 +126,13 @@ export interface CompilerHost extends ts.CompilerHost { /** * Converts a file path to a module name that can be used as an `import ...` * I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`. - * - * See ImportResolver. */ fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null; + /** + * Converts a file path for a resource that is used in a source file or another resource + * into a filepath. + */ + resourceNameToFileName(resourceName: string, containingFilePath: string): string|null; /** * Converts a file name into a representation that should be stored in a summary file. * This has to include changing the suffix as well. diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index a6881e6f24..74b319838d 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -26,6 +26,7 @@ export function createCompilerHost( host.fileNameToModuleName = mixin.fileNameToModuleName.bind(mixin); host.toSummaryFileName = mixin.toSummaryFileName.bind(mixin); host.fromSummaryFileName = mixin.fromSummaryFileName.bind(mixin); + host.resourceNameToFileName = mixin.resourceNameToFileName.bind(mixin); // Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks. // https://github.com/Microsoft/TypeScript/issues/9552 @@ -35,26 +36,23 @@ export function createCompilerHost( } class CompilerHostMixin { - private moduleFileNames = new Map(); private rootDirs: string[]; private basePath: string; private moduleResolutionHost: ModuleFilenameResolutionHost; + private moduleResolutionCache: ts.ModuleResolutionCache; - constructor(private context: ts.ModuleResolutionHost, private options: CompilerOptions) { + constructor(private context: ts.CompilerHost, private options: CompilerOptions) { // normalize the path so that it never ends with '/'. this.basePath = normalizePath(this.options.basePath !); this.rootDirs = (this.options.rootDirs || [ this.options.basePath ! ]).map(p => path.resolve(this.basePath, normalizePath(p))); this.moduleResolutionHost = createModuleFilenameResolverHost(context); + this.moduleResolutionCache = ts.createModuleResolutionCache( + this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context)); } moduleNameToFileName(m: string, containingFile: string): string|null { - const key = m + ':' + (containingFile || ''); - let result: string|null = this.moduleFileNames.get(key) || null; - if (result) { - return result; - } if (!containingFile) { if (m.indexOf('.') === 0) { throw new Error('Resolution of relative paths requires a containing file.'); @@ -62,17 +60,17 @@ class CompilerHostMixin { // Any containing file gives the same result for absolute imports containingFile = path.join(this.basePath, 'index.ts'); } - const resolved = - ts.resolveModuleName(m, containingFile, this.options, this.moduleResolutionHost) - .resolvedModule; + const resolved = ts.resolveModuleName( + m, containingFile, this.options, this.moduleResolutionHost, + this.moduleResolutionCache) + .resolvedModule; if (resolved) { if (this.options.traceResolution) { console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName); } - result = resolved.resolvedFileName; + return resolved.resolvedFileName; } - this.moduleFileNames.set(key, result); - return result; + return null; } /** @@ -140,6 +138,17 @@ class CompilerHostMixin { } return resolved; } + + resourceNameToFileName(resourceName: string, containingFile: string): string|null { + // Note: we convert package paths into relative paths to be compatible with the the + // previous implementation of UrlResolver. + if (resourceName && resourceName.charAt(0) !== '.' && !path.isAbsolute(resourceName)) { + resourceName = `./${resourceName}`; + } + const filePathWithNgResource = + this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile); + return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null; + } } interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost { @@ -156,6 +165,7 @@ function createModuleFilenameResolverHost(host: ts.ModuleResolutionHost): // This is needed as we use ts.resolveModuleName in DefaultModuleFilenameResolver // and it should be able to resolve summary file names. resolveModuleNameHost.fileExists = (fileName: string): boolean => { + fileName = stripNgResourceSuffix(fileName); if (assumedExists.has(fileName)) { return true; } @@ -216,4 +226,12 @@ function getNodeModulesPrefix(filePath: string): string|null { function normalizePath(p: string): string { return path.normalize(path.join(p, '.')).replace(/\\/g, '/'); -} \ No newline at end of file +} + +function stripNgResourceSuffix(fileName: string): string { + return fileName.replace(/\.\$ngresource\$.*/, ''); +} + +function addNgResourceSuffix(fileName: string): string { + return `${fileName}.$ngresource$`; +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index e9c81d6b30..75ebebcd3e 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -315,6 +315,10 @@ class AotCompilerHostImpl extends BaseAotCompilerHost { return this.context.fileNameToModuleName(importedFile, containingFile); } + resourceNameToFileName(resourceName: string, containingFile: string): string|null { + return this.context.resourceNameToFileName(resourceName, containingFile); + } + toSummaryFileName(fileName: string, referringSrcFileName: string): string { return this.context.toSummaryFileName(fileName, referringSrcFileName); } diff --git a/packages/compiler-cli/test/transformers/compiler_host_spec.ts b/packages/compiler-cli/test/transformers/compiler_host_spec.ts index 0d226420e6..9d86f460c6 100644 --- a/packages/compiler-cli/test/transformers/compiler_host_spec.ts +++ b/packages/compiler-cli/test/transformers/compiler_host_spec.ts @@ -100,4 +100,27 @@ describe('NgCompilerHost', () => { .toBe('/tmp/src/a/child.d.ts'); }); }); -}); \ No newline at end of file + + describe('resourceNameToFileName', () => { + it('should resolve a relative import', () => { + const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '
'}}}}}); + expect(ngHost.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts')) + .toBe('/tmp/src/a/child.html'); + + expect(ngHost.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts')) + .toBe(null); + }); + + it('should resolve package paths as relative paths', () => { + const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '
'}}}}}); + expect(ngHost.resourceNameToFileName('a/child.html', '/tmp/src/index.ts')) + .toBe('/tmp/src/a/child.html'); + }); + + it('should resolve absolute paths', () => { + const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '
'}}}}}); + expect(ngHost.resourceNameToFileName('/tmp/src/a/child.html', '/tmp/src/index.ts')) + .toBe('/tmp/src/a/child.html'); + }); + }); +}); diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index cbdbc806ec..d3a350fc9d 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -7,6 +7,7 @@ */ import {MissingTranslationStrategy, ViewEncapsulation, ɵConsole as Console} from '@angular/core'; + import {CompilerConfig} from '../config'; import {DirectiveNormalizer} from '../directive_normalizer'; import {DirectiveResolver} from '../directive_resolver'; @@ -22,7 +23,8 @@ import {PipeResolver} from '../pipe_resolver'; import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry'; import {StyleCompiler} from '../style_compiler'; import {TemplateParser} from '../template_parser/template_parser'; -import {createOfflineCompileUrlResolver} from '../url_resolver'; +import {UrlResolver} from '../url_resolver'; +import {syntaxError} from '../util'; import {ViewCompiler} from '../view_compiler/view_compiler'; import {AotCompiler} from './compiler'; @@ -33,6 +35,19 @@ import {StaticSymbol, StaticSymbolCache} from './static_symbol'; import {StaticSymbolResolver} from './static_symbol_resolver'; import {AotSummaryResolver} from './summary_resolver'; +export function createAotUrlResolver(host: { + resourceNameToFileName(resourceName: string, containingFileName: string): string | null; +}): UrlResolver { + return { + resolve: (basePath: string, url: string) => { + const filePath = host.resourceNameToFileName(url, basePath); + if (!filePath) { + throw syntaxError(`Couldn't resolve resource ${url} from ${basePath}`); + } + return filePath; + } + }; +} /** * Creates a new AotCompiler based on options and a host. @@ -41,7 +56,7 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom {compiler: AotCompiler, reflector: StaticReflector} { let translations: string = options.translations || ''; - const urlResolver = createOfflineCompileUrlResolver(); + const urlResolver = createAotUrlResolver(compilerHost); const symbolCache = new StaticSymbolCache(); const summaryResolver = new AotSummaryResolver(compilerHost, symbolCache); const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver); diff --git a/packages/compiler/src/aot/compiler_host.ts b/packages/compiler/src/aot/compiler_host.ts index e5c2927845..00bbf1e45b 100644 --- a/packages/compiler/src/aot/compiler_host.ts +++ b/packages/compiler/src/aot/compiler_host.ts @@ -14,6 +14,11 @@ import {AotSummaryResolverHost} from './summary_resolver'; * services and from underlying file systems. */ export interface AotCompilerHost extends StaticSymbolResolverHost, AotSummaryResolverHost { + /** + * Converts a path that refers to a resource into an absolute filePath + * that can be later on used for loading the resource via `loadResource. + */ + resourceNameToFileName(resourceName: string, containingFileName: string): string|null; /** * Loads a resource (e.g. html / css) */ diff --git a/packages/compiler/src/i18n/extractor.ts b/packages/compiler/src/i18n/extractor.ts index 5d6cbb5d1f..526c04f203 100644 --- a/packages/compiler/src/i18n/extractor.ts +++ b/packages/compiler/src/i18n/extractor.ts @@ -13,6 +13,7 @@ import {ViewEncapsulation, ɵConsole as Console} from '@angular/core'; import {analyzeAndValidateNgModules, extractProgramSymbols} from '../aot/compiler'; +import {createAotUrlResolver} from '../aot/compiler_factory'; import {StaticReflector} from '../aot/static_reflector'; import {StaticSymbolCache} from '../aot/static_symbol'; import {StaticSymbolResolver, StaticSymbolResolverHost} from '../aot/static_symbol_resolver'; @@ -28,14 +29,21 @@ import {NgModuleResolver} from '../ng_module_resolver'; import {ParseError} from '../parse_util'; import {PipeResolver} from '../pipe_resolver'; import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry'; -import {createOfflineCompileUrlResolver} from '../url_resolver'; +import {syntaxError} from '../util'; + import {MessageBundle} from './message_bundle'; + /** * The host of the Extractor disconnects the implementation from TypeScript / other language * services and from underlying file systems. */ export interface ExtractorHost extends StaticSymbolResolverHost, AotSummaryResolverHost { + /** + * Converts a path that refers to a resource into an absolute filePath + * that can be lateron used for loading the resource via `loadResource. + */ + resourceNameToFileName(path: string, containingFile: string): string|null; /** * Loads a resource (e.g. html / css) */ @@ -87,7 +95,7 @@ export class Extractor { {extractor: Extractor, staticReflector: StaticReflector} { const htmlParser = new HtmlParser(); - const urlResolver = createOfflineCompileUrlResolver(); + const urlResolver = createAotUrlResolver(host); const symbolCache = new StaticSymbolCache(); const summaryResolver = new AotSummaryResolver(host, symbolCache); const staticSymbolResolver = new StaticSymbolResolver(host, symbolCache, summaryResolver); diff --git a/packages/compiler/src/url_resolver.ts b/packages/compiler/src/url_resolver.ts index b72fb3a2c9..ba585c1c65 100644 --- a/packages/compiler/src/url_resolver.ts +++ b/packages/compiler/src/url_resolver.ts @@ -11,14 +11,6 @@ import {Inject, InjectionToken, PACKAGE_ROOT_URL} from '@angular/core'; import {CompilerInjectable} from './injectable'; - -/** - * Create a {@link UrlResolver} with no package prefix. - */ -export function createUrlResolverWithoutPackagePrefix(): UrlResolver { - return new UrlResolver(); -} - export function createOfflineCompileUrlResolver(): UrlResolver { return new UrlResolver('.'); } @@ -47,9 +39,12 @@ export const DEFAULT_PACKAGE_URL_PROVIDER = { * Attacker-controlled data introduced by a template could expose your * application to XSS risks. For more detail, see the [Security Guide](http://g.co/ng/security). */ -@CompilerInjectable() -export class UrlResolver { - constructor(@Inject(PACKAGE_ROOT_URL) private _packagePrefix: string|null = null) {} +export interface UrlResolver { resolve(baseUrl: string, url: string): string; } + +export interface UrlResolverCtor { new (packagePrefix?: string|null): UrlResolver; } + +export const UrlResolver: UrlResolverCtor = class UrlResolverImpl { + constructor(private _packagePrefix: string|null = null) {} /** * Resolves the `url` given the `baseUrl`: @@ -75,7 +70,7 @@ export class UrlResolver { } return resolvedUrl; } -} +}; /** * Extract the scheme of a URL. diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 288aef60d1..8a28700497 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -330,8 +330,15 @@ export class MockAotCompilerHost implements AotCompilerHost { private metadataCollector = new MetadataCollector(); private metadataVisible: boolean = true; private dtsAreSource: boolean = true; + private resolveModuleNameHost: ts.ModuleResolutionHost; - constructor(private tsHost: MockCompilerHost) {} + constructor(private tsHost: MockCompilerHost) { + this.resolveModuleNameHost = Object.create(tsHost); + this.resolveModuleNameHost.fileExists = (fileName) => { + fileName = stripNgResourceSuffix(fileName); + return tsHost.fileExists(fileName); + }; + } hideMetadata() { this.metadataVisible = false; } @@ -369,11 +376,22 @@ export class MockAotCompilerHost implements AotCompilerHost { moduleName = moduleName.replace(EXT, ''); const resolved = ts.resolveModuleName( moduleName, containingFile.replace(/\\/g, '/'), - {baseDir: '/', genDir: '/'}, this.tsHost) + {baseDir: '/', genDir: '/'}, this.resolveModuleNameHost) .resolvedModule; return resolved ? resolved.resolvedFileName : null; } + resourceNameToFileName(resourceName: string, containingFile: string) { + // Note: we convert package paths into relative paths to be compatible with the the + // previous implementation of UrlResolver. + if (resourceName && resourceName.charAt(0) !== '.' && !path.isAbsolute(resourceName)) { + resourceName = `./${resourceName}`; + } + const filePathWithNgResource = + this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile); + return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null; + } + // AotSummaryResolverHost loadSummary(filePath: string): string|null { return this.tsHost.readFile(filePath); } @@ -647,3 +665,11 @@ export function compile( } return {genFiles, outDir}; } + +function stripNgResourceSuffix(fileName: string): string { + return fileName.replace(/\.\$ngresource\$.*/, ''); +} + +function addNgResourceSuffix(fileName: string): string { + return `${fileName}.$ngresource$`; +}