refactor(ivy): expose resolving URLs in the ResourceLoader (#28199)

Resources can be loaded in the context of another file, which
means that the path to the resource file must be resolved
before it can be loaded.

Previously the API of this interface did not allow the client
code to get access to the resolved URL which is used to load
the resource.

Now this API has been refactored so that you must do the
resource URL resolving first and the loading expects a
resolved URL.

PR Close #28199
This commit is contained in:
Pete Bacon Darwin 2019-01-16 17:22:53 +00:00 committed by Alex Rickabaugh
parent 7bdf3fe41c
commit 73dcd72afb
6 changed files with 192 additions and 101 deletions

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool} from '@angular/compiler'; 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 path from 'canonical-path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations'; 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 {CompileResult, DecoratorHandler} from '../../../ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class'; import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
@ -46,12 +46,14 @@ export interface MatchingHandler<A, M> {
} }
/** /**
* `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 { class NgccResourceLoader implements ResourceLoader {
load(url: string, containingFile: string): string { canPreload = false;
url = path.resolve(path.dirname(containingFile), url); preload(): undefined|Promise<void> { throw new Error('Not implemented.'); }
return fs.readFileSync(url, 'utf8'); 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. * This Analyzer will analyze the files that have decorated classes that need to be transformed.
*/ */
export class DecorationAnalyzer { export class DecorationAnalyzer {
resourceLoader = new FileResourceLoader(); resourceManager = new NgccResourceLoader();
resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host); resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host);
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver); scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver); evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver);
handlers: DecoratorHandler<any, any>[] = [ handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator), new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator),
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceLoader, this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager,
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true), this.rootDirs, /* defaultPreserveWhitespaces */ false,
/* i18nUseExternalIds */ true),
new DirectiveDecoratorHandler( new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore), this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore),
new InjectableDecoratorHandler(this.reflectionHost, this.isCore), new InjectableDecoratorHandler(this.reflectionHost, this.isCore),

View File

@ -6,7 +6,50 @@
* found in the LICENSE file at https://angular.io/license * 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 { export interface ResourceLoader {
preload?(url: string, containingFile: string): Promise<void>|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<void>|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;
} }

View File

@ -56,33 +56,40 @@ export class ComponentDecoratorHandler implements
} }
preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise<void>|undefined { preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise<void>|undefined {
if (!this.resourceLoader.canPreload) {
return undefined;
}
const meta = this._resolveLiteral(decorator); const meta = this._resolveLiteral(decorator);
const component = reflectObjectLiteral(meta); const component = reflectObjectLiteral(meta);
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const containingFile = node.getSourceFile().fileName; const containingFile = node.getSourceFile().fileName;
if (this.resourceLoader.preload !== undefined && component.has('templateUrl')) { if (component.has('templateUrl')) {
const templateUrlExpr = component.get('templateUrl') !; const templateUrlExpr = component.get('templateUrl') !;
const templateUrl = this.evaluator.evaluate(templateUrlExpr); const templateUrl = this.evaluator.evaluate(templateUrlExpr);
if (typeof templateUrl !== 'string') { if (typeof templateUrl !== 'string') {
throw new FatalDiagnosticError( throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string'); 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) { if (promise !== undefined) {
promises.push(promise); promises.push(promise);
} }
} }
const styleUrls = this._extractStyleUrls(component); const styleUrls = this._extractStyleUrls(component);
if (this.resourceLoader.preload !== undefined && styleUrls !== null) { if (styleUrls !== null) {
for (const styleUrl of styleUrls) { 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) { if (promise !== undefined) {
promises.push(promise); promises.push(promise);
} }
} }
} }
if (promises.length !== 0) { if (promises.length !== 0) {
return Promise.all(promises).then(() => undefined); return Promise.all(promises).then(() => undefined);
} else { } else {
@ -118,7 +125,8 @@ export class ComponentDecoratorHandler implements
throw new FatalDiagnosticError( throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string'); 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')) { } else if (component.has('template')) {
const templateExpr = component.get('template') !; const templateExpr = component.get('template') !;
const resolvedTemplate = this.evaluator.evaluate(templateExpr); const resolvedTemplate = this.evaluator.evaluate(templateExpr);
@ -223,7 +231,10 @@ export class ComponentDecoratorHandler implements
if (styles === null) { if (styles === null) {
styles = []; 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 = const encapsulation: number =

View File

@ -18,7 +18,10 @@ import {ComponentDecoratorHandler} from '../src/component';
import {SelectorScopeRegistry} from '../src/selector_scope'; import {SelectorScopeRegistry} from '../src/selector_scope';
export class NoopResourceLoader implements ResourceLoader { 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<void>|undefined { throw new Error('Not implemented'); }
} }
describe('ComponentDecoratorHandler', () => { describe('ComponentDecoratorHandler', () => {

View File

@ -12,14 +12,14 @@ import * as ts from 'typescript';
import * as api from '../transformers/api'; import * as api from '../transformers/api';
import {nocollapseHack} from '../transformers/nocollapse_hack'; 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 {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {ErrorCode, ngErrorCode} from './diagnostics'; import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports'; import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports';
import {PartialEvaluator} from './partial_evaluator'; import {PartialEvaluator} from './partial_evaluator';
import {TypeScriptReflectionHost} from './reflection'; 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 {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, generatedFactoryTransform} from './shims';
import {ivySwitchTransform} from './switch'; import {ivySwitchTransform} from './switch';
import {IvyCompilation, ivyTransformFactory} from './transform'; import {IvyCompilation, ivyTransformFactory} from './transform';
@ -28,7 +28,7 @@ import {isDtsPath} from './util/src/typescript';
export class NgtscProgram implements api.Program { export class NgtscProgram implements api.Program {
private tsProgram: ts.Program; private tsProgram: ts.Program;
private resourceLoader: ResourceLoader; private resourceManager: HostResourceLoader;
private compilation: IvyCompilation|undefined = undefined; private compilation: IvyCompilation|undefined = undefined;
private factoryToSourceInfo: Map<string, FactoryInfo>|null = null; private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
private sourceToFactorySymbols: Map<string, Set<string>>|null = null; private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
@ -58,11 +58,7 @@ export class NgtscProgram implements api.Program {
this.rootDirs.push(host.getCurrentDirectory()); this.rootDirs.push(host.getCurrentDirectory());
} }
this.closureCompilerEnabled = !!options.annotateForClosureCompiler; this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceLoader = this.resourceManager = new HostResourceLoader(host, options);
host.readResource !== undefined && host.resourceNameToFileName !== undefined ?
new HostResourceLoader(
host.resourceNameToFileName.bind(host), host.readResource.bind(host)) :
new FileResourceLoader(host, this.options);
const shouldGenerateShims = options.allowEmptyCodegenFiles || false; const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
this.host = host; this.host = host;
let rootFiles = [...rootNames]; let rootFiles = [...rootNames];
@ -294,8 +290,9 @@ export class NgtscProgram implements api.Program {
const handlers = [ const handlers = [
new BaseDefDecoratorHandler(this.reflector, evaluator), new BaseDefDecoratorHandler(this.reflector, evaluator),
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceLoader, this.rootDirs, this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager,
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false), this.rootDirs, this.options.preserveWhitespaces || false,
this.options.i18nUseExternalIds !== false),
new DirectiveDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore), new DirectiveDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore),
new InjectableDecoratorHandler(this.reflector, this.isCore), new InjectableDecoratorHandler(this.reflector, this.isCore),
new NgModuleDecoratorHandler( new NgModuleDecoratorHandler(

View File

@ -8,8 +8,8 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CompilerHost} from '../transformers/api';
import {ResourceLoader} from './annotations'; import {ResourceLoader} from './annotations/src/api';
/** /**
* `ResourceLoader` which delegates to a `CompilerHost` resource loading method. * `ResourceLoader` which delegates to a `CompilerHost` resource loading method.
@ -18,107 +18,141 @@ export class HostResourceLoader implements ResourceLoader {
private cache = new Map<string, string>(); private cache = new Map<string, string>();
private fetching = new Map<string, Promise<void>>(); private fetching = new Map<string, Promise<void>>();
constructor( canPreload = !!this.host.readResource;
private resolver: (file: string, basePath: string) => string | null,
private loader: (url: string) => string | Promise<string>) {}
preload(file: string, containingFile: string): Promise<void>|undefined { constructor(private host: CompilerHost, private options: ts.CompilerOptions) {}
const resolved = this.resolver(file, containingFile);
if (resolved === null) { /**
* 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<void>|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; return undefined;
} else if (this.fetching.has(resolvedUrl)) {
return this.fetching.get(resolvedUrl);
} }
if (this.cache.has(resolved)) { const result = this.host.readResource(resolvedUrl);
return undefined;
} else if (this.fetching.has(resolved)) {
return this.fetching.get(resolved);
}
const result = this.loader(resolved);
if (typeof result === 'string') { if (typeof result === 'string') {
this.cache.set(resolved, result); this.cache.set(resolvedUrl, result);
return undefined; return undefined;
} else { } else {
const fetchCompletion = result.then(str => { const fetchCompletion = result.then(str => {
this.fetching.delete(resolved); this.fetching.delete(resolvedUrl);
this.cache.set(resolved, str); this.cache.set(resolvedUrl, str);
}); });
this.fetching.set(resolved, fetchCompletion); this.fetching.set(resolvedUrl, fetchCompletion);
return fetchCompletion; return fetchCompletion;
} }
} }
load(file: string, containingFile: string): string { /**
const resolved = this.resolver(file, containingFile); * Load the resource at the given url, synchronously.
if (resolved === null) { *
throw new Error( * The contents of the resource may have been cached by a previous call to `preload()`.
`HostResourceLoader: could not resolve ${file} in context of ${containingFile})`); *
* @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)) { const result = this.host.readResource ? this.host.readResource(resolvedUrl) :
return this.cache.get(resolved) !; fs.readFileSync(resolvedUrl, 'utf8');
}
const result = this.loader(resolved);
if (typeof result !== 'string') { 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; 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<string>};
/**
* `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. // Strip a leading '/' if one is present.
if (file.startsWith('/')) { if (url.startsWith('/')) {
file = file.substr(1); url = url.substr(1);
} }
// Turn absolute paths into relative paths. // Turn absolute paths into relative paths.
if (!file.startsWith('.')) { if (!url.startsWith('.')) {
file = `./${file}`; url = `./${url}`;
} }
// TypeScript provides utilities to resolve module names, but not resource files (which aren't const candidateLocations = this.getCandidateLocations(url, fromFile);
// a part of the ts.Program). However, TypeScript's module resolution can be used creatively for (const candidate of candidateLocations) {
// to locate where resource files should be expected to exist. Since module resolution returns if (fs.existsSync(candidate)) {
// a list of file names that were considered, the loader can enumerate the possible locations return candidate;
// for the file by setting up a module resolution for it that will fail. }
file += '.$ngresource$'; }
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<string>};
// clang-format off // 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 // clang-format on
if (failedLookup.failedLookupLocations === undefined) { if (failedLookup.failedLookupLocations === undefined) {
throw new Error( 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 = return failedLookup.failedLookupLocations
failedLookup.failedLookupLocations .filter(candidate => candidate.endsWith('.$ngresource$.ts'))
.filter(candidate => candidate.endsWith('.$ngresource$.ts')) .map(candidate => candidate.replace(/\.\$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}`);
} }
} }