diff --git a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts index f24b0a7bbc..4961707644 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ import {ConstantPool} from '@angular/compiler'; -import {TsReferenceResolver} from '@angular/compiler-cli/src/ngtsc/imports'; -import {PartialEvaluator} from '@angular/compiler-cli/src/ngtsc/partial_evaluator'; import * as path from 'canonical-path'; import * as fs from 'fs'; import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations'; +import {TsReferenceResolver} from '../../../ngtsc/imports'; +import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; import {CompileResult, DecoratorHandler} from '../../../ngtsc/transform'; import {DecoratedClass} from '../host/decorated_class'; import {NgccReflectionHost} from '../host/ngcc_host'; @@ -46,12 +46,14 @@ export interface MatchingHandler { } /** - * `ResourceLoader` which directly uses the filesystem to resolve resources synchronously. + * Simple class that resolves and loads files directly from the filesystem. */ -export class FileResourceLoader implements ResourceLoader { - load(url: string, containingFile: string): string { - url = path.resolve(path.dirname(containingFile), url); - return fs.readFileSync(url, 'utf8'); +class NgccResourceLoader implements ResourceLoader { + canPreload = false; + preload(): undefined|Promise { throw new Error('Not implemented.'); } + load(url: string): string { return fs.readFileSync(url, 'utf8'); } + resolve(url: string, containingFile: string): string { + return path.resolve(path.dirname(containingFile), url); } } @@ -59,15 +61,16 @@ export class FileResourceLoader implements ResourceLoader { * This Analyzer will analyze the files that have decorated classes that need to be transformed. */ export class DecorationAnalyzer { - resourceLoader = new FileResourceLoader(); + resourceManager = new NgccResourceLoader(); resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host); scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver); evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver); handlers: DecoratorHandler[] = [ new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator), new ComponentDecoratorHandler( - this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceLoader, - this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true), + this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager, + this.rootDirs, /* defaultPreserveWhitespaces */ false, + /* i18nUseExternalIds */ true), new DirectiveDecoratorHandler( this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore), new InjectableDecoratorHandler(this.reflectionHost, this.isCore), diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/api.ts b/packages/compiler-cli/src/ngtsc/annotations/src/api.ts index 10b156d031..364cfdaf0c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/api.ts @@ -6,7 +6,50 @@ * found in the LICENSE file at https://angular.io/license */ +/** + * Resolves and loads resource files that are referenced in Angular metadata. + * + * Note that `preload()` and `load()` take a `resolvedUrl`, which can be found + * by calling `resolve()`. These two steps are separated because sometimes the + * resolved URL to the resource is needed as well as its contents. + */ export interface ResourceLoader { - preload?(url: string, containingFile: string): Promise|undefined; - load(url: string, containingFile: string): string; + /** + * True if this resource loader can preload resources. + * + * Sometimes a `ResourceLoader` is not able to do asynchronous pre-loading of resources. + */ + canPreload: boolean; + + /** + * Resolve the url of a resource relative to the file that contains the reference to it. + * The return value of this method can be used in the `load()` and `preload()` methods. + * + * @param url The, possibly relative, url of the resource. + * @param fromFile The path to the file that contains the URL of the resource. + * @returns A resolved url of resource. + * @throws An error if the resource cannot be resolved. + */ + resolve(file: string, basePath: string): string; + + /** + * Preload the specified resource, asynchronously. Once the resource is loaded, its value + * should be cached so it can be accessed synchronously via the `load()` method. + * + * @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to preload. + * @returns A Promise that is resolved once the resource has been loaded or `undefined` + * if the file has already been loaded. + * @throws An Error if pre-loading is not available. + */ + preload(resolvedUrl: string): Promise|undefined; + + /** + * Load the resource at the given url, synchronously. + * + * The contents of the resource may have been cached by a previous call to `preload()`. + * + * @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to load. + * @returns The contents of the resource. + */ + load(resolvedUrl: string): string; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 54ad077323..418da51c9b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -56,33 +56,40 @@ export class ComponentDecoratorHandler implements } preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise|undefined { + if (!this.resourceLoader.canPreload) { + return undefined; + } + const meta = this._resolveLiteral(decorator); const component = reflectObjectLiteral(meta); const promises: Promise[] = []; const containingFile = node.getSourceFile().fileName; - if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) { + if (component.has('templateUrl')) { const templateUrlExpr = component.get('templateUrl') !; const templateUrl = this.evaluator.evaluate(templateUrlExpr); if (typeof templateUrl !== 'string') { throw new FatalDiagnosticError( ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string'); } - const promise = this.resourceLoader.preload(templateUrl, containingFile); + const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile); + const promise = this.resourceLoader.preload(resourceUrl); if (promise !== undefined) { promises.push(promise); } } const styleUrls = this._extractStyleUrls(component); - if (this.resourceLoader.preload !== undefined && styleUrls !== null) { + if (styleUrls !== null) { for (const styleUrl of styleUrls) { - const promise = this.resourceLoader.preload(styleUrl, containingFile); + const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile); + const promise = this.resourceLoader.preload(resourceUrl); if (promise !== undefined) { promises.push(promise); } } } + if (promises.length !== 0) { return Promise.all(promises).then(() => undefined); } else { @@ -118,7 +125,8 @@ export class ComponentDecoratorHandler implements throw new FatalDiagnosticError( ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string'); } - templateStr = this.resourceLoader.load(templateUrl, containingFile); + const resolvedTemplateUrl = this.resourceLoader.resolve(templateUrl, containingFile); + templateStr = this.resourceLoader.load(resolvedTemplateUrl); } else if (component.has('template')) { const templateExpr = component.get('template') !; const resolvedTemplate = this.evaluator.evaluate(templateExpr); @@ -223,7 +231,10 @@ export class ComponentDecoratorHandler implements if (styles === null) { styles = []; } - styles.push(...styleUrls.map(styleUrl => this.resourceLoader.load(styleUrl, containingFile))); + styleUrls.forEach(styleUrl => { + const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile); + styles !.push(this.resourceLoader.load(resourceUrl)); + }); } const encapsulation: number = diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index 05a702c433..40ceb55e41 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -18,7 +18,10 @@ import {ComponentDecoratorHandler} from '../src/component'; import {SelectorScopeRegistry} from '../src/selector_scope'; export class NoopResourceLoader implements ResourceLoader { - load(url: string): string { throw new Error('Not implemented'); } + resolve(): string { throw new Error('Not implemented.'); } + canPreload = false; + load(): string { throw new Error('Not implemented'); } + preload(): Promise|undefined { throw new Error('Not implemented'); } } describe('ComponentDecoratorHandler', () => { diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index f87e880d4c..7cdb18d0cd 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -12,14 +12,14 @@ import * as ts from 'typescript'; import * as api from '../transformers/api'; import {nocollapseHack} from '../transformers/nocollapse_hack'; -import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from './annotations'; +import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry, SelectorScopeRegistry} from './annotations'; import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports'; import {PartialEvaluator} from './partial_evaluator'; import {TypeScriptReflectionHost} from './reflection'; -import {FileResourceLoader, HostResourceLoader} from './resource_loader'; +import {HostResourceLoader} from './resource_loader'; import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, generatedFactoryTransform} from './shims'; import {ivySwitchTransform} from './switch'; import {IvyCompilation, ivyTransformFactory} from './transform'; @@ -28,7 +28,7 @@ import {isDtsPath} from './util/src/typescript'; export class NgtscProgram implements api.Program { private tsProgram: ts.Program; - private resourceLoader: ResourceLoader; + private resourceManager: HostResourceLoader; private compilation: IvyCompilation|undefined = undefined; private factoryToSourceInfo: Map|null = null; private sourceToFactorySymbols: Map>|null = null; @@ -58,11 +58,7 @@ export class NgtscProgram implements api.Program { this.rootDirs.push(host.getCurrentDirectory()); } this.closureCompilerEnabled = !!options.annotateForClosureCompiler; - this.resourceLoader = - host.readResource !== undefined && host.resourceNameToFileName !== undefined ? - new HostResourceLoader( - host.resourceNameToFileName.bind(host), host.readResource.bind(host)) : - new FileResourceLoader(host, this.options); + this.resourceManager = new HostResourceLoader(host, options); const shouldGenerateShims = options.allowEmptyCodegenFiles || false; this.host = host; let rootFiles = [...rootNames]; @@ -294,8 +290,9 @@ export class NgtscProgram implements api.Program { const handlers = [ new BaseDefDecoratorHandler(this.reflector, evaluator), new ComponentDecoratorHandler( - this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceLoader, this.rootDirs, - this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false), + this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager, + this.rootDirs, this.options.preserveWhitespaces || false, + this.options.i18nUseExternalIds !== false), new DirectiveDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore), new InjectableDecoratorHandler(this.reflector, this.isCore), new NgModuleDecoratorHandler( diff --git a/packages/compiler-cli/src/ngtsc/resource_loader.ts b/packages/compiler-cli/src/ngtsc/resource_loader.ts index 5986795d97..f728565ff8 100644 --- a/packages/compiler-cli/src/ngtsc/resource_loader.ts +++ b/packages/compiler-cli/src/ngtsc/resource_loader.ts @@ -8,8 +8,8 @@ import * as fs from 'fs'; import * as ts from 'typescript'; - -import {ResourceLoader} from './annotations'; +import {CompilerHost} from '../transformers/api'; +import {ResourceLoader} from './annotations/src/api'; /** * `ResourceLoader` which delegates to a `CompilerHost` resource loading method. @@ -18,107 +18,141 @@ export class HostResourceLoader implements ResourceLoader { private cache = new Map(); private fetching = new Map>(); - constructor( - private resolver: (file: string, basePath: string) => string | null, - private loader: (url: string) => string | Promise) {} + canPreload = !!this.host.readResource; - preload(file: string, containingFile: string): Promise|undefined { - const resolved = this.resolver(file, containingFile); - if (resolved === null) { + constructor(private host: CompilerHost, private options: ts.CompilerOptions) {} + + /** + * Resolve the url of a resource relative to the file that contains the reference to it. + * The return value of this method can be used in the `load()` and `preload()` methods. + * + * Uses the provided CompilerHost if it supports mapping resources to filenames. + * Otherwise, uses a fallback mechanism that searches the module resolution candidates. + * + * @param url The, possibly relative, url of the resource. + * @param fromFile The path to the file that contains the URL of the resource. + * @returns A resolved url of resource. + * @throws An error if the resource cannot be resolved. + */ + resolve(url: string, fromFile: string): string { + let resolvedUrl: string|null = null; + if (this.host.resourceNameToFileName) { + resolvedUrl = this.host.resourceNameToFileName(url, fromFile); + } else { + resolvedUrl = this.fallbackResolve(url, fromFile); + } + if (resolvedUrl === null) { + throw new Error(`HostResourceResolver: could not resolve ${url} in context of ${fromFile})`); + } + return resolvedUrl; + } + + /** + * Preload the specified resource, asynchronously. + * + * Once the resource is loaded, its value is cached so it can be accessed synchronously via the + * `load()` method. + * + * @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to preload. + * @returns A Promise that is resolved once the resource has been loaded or `undefined` if the + * file has already been loaded. + * @throws An Error if pre-loading is not available. + */ + preload(resolvedUrl: string): Promise|undefined { + if (!this.host.readResource) { + throw new Error( + 'HostResourceLoader: the CompilerHost provided does not support pre-loading resources.'); + } + if (this.cache.has(resolvedUrl)) { return undefined; + } else if (this.fetching.has(resolvedUrl)) { + return this.fetching.get(resolvedUrl); } - if (this.cache.has(resolved)) { - return undefined; - } else if (this.fetching.has(resolved)) { - return this.fetching.get(resolved); - } - - const result = this.loader(resolved); + const result = this.host.readResource(resolvedUrl); if (typeof result === 'string') { - this.cache.set(resolved, result); + this.cache.set(resolvedUrl, result); return undefined; } else { const fetchCompletion = result.then(str => { - this.fetching.delete(resolved); - this.cache.set(resolved, str); + this.fetching.delete(resolvedUrl); + this.cache.set(resolvedUrl, str); }); - this.fetching.set(resolved, fetchCompletion); + this.fetching.set(resolvedUrl, fetchCompletion); return fetchCompletion; } } - load(file: string, containingFile: string): string { - const resolved = this.resolver(file, containingFile); - if (resolved === null) { - throw new Error( - `HostResourceLoader: could not resolve ${file} in context of ${containingFile})`); + /** + * Load the resource at the given url, synchronously. + * + * The contents of the resource may have been cached by a previous call to `preload()`. + * + * @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to load. + * @returns The contents of the resource. + */ + load(resolvedUrl: string): string { + if (this.cache.has(resolvedUrl)) { + return this.cache.get(resolvedUrl) !; } - if (this.cache.has(resolved)) { - return this.cache.get(resolved) !; - } - - const result = this.loader(resolved); + const result = this.host.readResource ? this.host.readResource(resolvedUrl) : + fs.readFileSync(resolvedUrl, 'utf8'); if (typeof result !== 'string') { - throw new Error(`HostResourceLoader: loader(${resolved}) returned a Promise`); + throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`); } - this.cache.set(resolved, result); + this.cache.set(resolvedUrl, result); return result; } -} - - - -// `failedLookupLocations` is in the name of the type ts.ResolvedModuleWithFailedLookupLocations -// but is marked @internal in TypeScript. See https://github.com/Microsoft/TypeScript/issues/28770. -type ResolvedModuleWithFailedLookupLocations = - ts.ResolvedModuleWithFailedLookupLocations & {failedLookupLocations: ReadonlyArray}; - -/** - * `ResourceLoader` which directly uses the filesystem to resolve resources synchronously. - */ -export class FileResourceLoader implements ResourceLoader { - constructor(private host: ts.CompilerHost, private options: ts.CompilerOptions) {} - - load(file: string, containingFile: string): string { - // Attempt to resolve `file` in the context of `containingFile`, while respecting the rootDirs - // option from the tsconfig. First, normalize the file name. + /** + * Attempt to resolve `url` in the context of `fromFile`, while respecting the rootDirs + * option from the tsconfig. First, normalize the file name. + */ + private fallbackResolve(url: string, fromFile: string): string|null { // Strip a leading '/' if one is present. - if (file.startsWith('/')) { - file = file.substr(1); + if (url.startsWith('/')) { + url = url.substr(1); } // Turn absolute paths into relative paths. - if (!file.startsWith('.')) { - file = `./${file}`; + if (!url.startsWith('.')) { + url = `./${url}`; } - // TypeScript provides utilities to resolve module names, but not resource files (which aren't - // a part of the ts.Program). However, TypeScript's module resolution can be used creatively - // to locate where resource files should be expected to exist. Since module resolution returns - // a list of file names that were considered, the loader can enumerate the possible locations - // for the file by setting up a module resolution for it that will fail. - file += '.$ngresource$'; + const candidateLocations = this.getCandidateLocations(url, fromFile); + for (const candidate of candidateLocations) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; + } + + + /** + * TypeScript provides utilities to resolve module names, but not resource files (which aren't + * a part of the ts.Program). However, TypeScript's module resolution can be used creatively + * to locate where resource files should be expected to exist. Since module resolution returns + * a list of file names that were considered, the loader can enumerate the possible locations + * for the file by setting up a module resolution for it that will fail. + */ + private getCandidateLocations(url: string, fromFile: string): string[] { + // `failedLookupLocations` is in the name of the type ts.ResolvedModuleWithFailedLookupLocations + // but is marked @internal in TypeScript. See + // https://github.com/Microsoft/TypeScript/issues/28770. + type ResolvedModuleWithFailedLookupLocations = + ts.ResolvedModuleWithFailedLookupLocations & {failedLookupLocations: ReadonlyArray}; // clang-format off - const failedLookup = ts.resolveModuleName(file, containingFile, this.options, this.host) as ResolvedModuleWithFailedLookupLocations; + const failedLookup = ts.resolveModuleName(url + '.$ngresource$', fromFile, this.options, this.host) as ResolvedModuleWithFailedLookupLocations; // clang-format on if (failedLookup.failedLookupLocations === undefined) { throw new Error( - `Internal error: expected to find failedLookupLocations during resolution of resource '${file}' in context of ${containingFile}`); + `Internal error: expected to find failedLookupLocations during resolution of resource '${url}' in context of ${fromFile}`); } - const candidateLocations = - failedLookup.failedLookupLocations - .filter(candidate => candidate.endsWith('.$ngresource$.ts')) - .map(candidate => candidate.replace(/\.\$ngresource\$\.ts$/, '')); - - for (const candidate of candidateLocations) { - if (fs.existsSync(candidate)) { - return fs.readFileSync(candidate, 'utf8'); - } - } - throw new Error(`Could not find resource ${file} in context of ${containingFile}`); + return failedLookup.failedLookupLocations + .filter(candidate => candidate.endsWith('.$ngresource$.ts')) + .map(candidate => candidate.replace(/\.\$ngresource\$\.ts$/, '')); } }