2018-06-26 15:01:09 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
|
*
|
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as fs from 'fs';
|
2018-11-30 10:37:06 -08:00
|
|
|
import * as ts from 'typescript';
|
2019-01-16 17:22:53 +00:00
|
|
|
import {CompilerHost} from '../transformers/api';
|
|
|
|
|
import {ResourceLoader} from './annotations/src/api';
|
2018-06-26 15:01:09 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* `ResourceLoader` which delegates to a `CompilerHost` resource loading method.
|
|
|
|
|
*/
|
|
|
|
|
export class HostResourceLoader implements ResourceLoader {
|
|
|
|
|
private cache = new Map<string, string>();
|
2018-11-18 22:04:43 +01:00
|
|
|
private fetching = new Map<string, Promise<void>>();
|
2018-06-26 15:01:09 -07:00
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
canPreload = !!this.host.readResource;
|
|
|
|
|
|
|
|
|
|
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);
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
2019-01-16 17:22:53 +00:00
|
|
|
if (resolvedUrl === null) {
|
|
|
|
|
throw new Error(`HostResourceResolver: could not resolve ${url} in context of ${fromFile})`);
|
|
|
|
|
}
|
|
|
|
|
return resolvedUrl;
|
|
|
|
|
}
|
2018-06-26 15:01:09 -07:00
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
/**
|
|
|
|
|
* 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)) {
|
2018-11-30 10:37:06 -08:00
|
|
|
return undefined;
|
2019-01-16 17:22:53 +00:00
|
|
|
} else if (this.fetching.has(resolvedUrl)) {
|
|
|
|
|
return this.fetching.get(resolvedUrl);
|
2018-11-30 10:37:06 -08:00
|
|
|
}
|
|
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
const result = this.host.readResource(resolvedUrl);
|
2018-06-26 15:01:09 -07:00
|
|
|
if (typeof result === 'string') {
|
2019-01-16 17:22:53 +00:00
|
|
|
this.cache.set(resolvedUrl, result);
|
2018-06-26 15:01:09 -07:00
|
|
|
return undefined;
|
|
|
|
|
} else {
|
2018-11-18 22:04:43 +01:00
|
|
|
const fetchCompletion = result.then(str => {
|
2019-01-16 17:22:53 +00:00
|
|
|
this.fetching.delete(resolvedUrl);
|
|
|
|
|
this.cache.set(resolvedUrl, str);
|
2018-06-26 15:01:09 -07:00
|
|
|
});
|
2019-01-16 17:22:53 +00:00
|
|
|
this.fetching.set(resolvedUrl, fetchCompletion);
|
2018-11-18 22:04:43 +01:00
|
|
|
return fetchCompletion;
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
/**
|
|
|
|
|
* 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) !;
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
|
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
const result = this.host.readResource ? this.host.readResource(resolvedUrl) :
|
|
|
|
|
fs.readFileSync(resolvedUrl, 'utf8');
|
2018-06-26 15:01:09 -07:00
|
|
|
if (typeof result !== 'string') {
|
2019-01-16 17:22:53 +00:00
|
|
|
throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`);
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
2019-01-16 17:22:53 +00:00
|
|
|
this.cache.set(resolvedUrl, result);
|
2018-06-26 15:01:09 -07:00
|
|
|
return result;
|
|
|
|
|
}
|
2018-11-30 10:37:06 -08:00
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
/**
|
|
|
|
|
* 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 {
|
2018-11-30 10:37:06 -08:00
|
|
|
// Strip a leading '/' if one is present.
|
2019-01-16 17:22:53 +00:00
|
|
|
if (url.startsWith('/')) {
|
|
|
|
|
url = url.substr(1);
|
2018-11-30 10:37:06 -08:00
|
|
|
}
|
|
|
|
|
// Turn absolute paths into relative paths.
|
2019-01-16 17:22:53 +00:00
|
|
|
if (!url.startsWith('.')) {
|
|
|
|
|
url = `./${url}`;
|
2018-11-30 10:37:06 -08:00
|
|
|
}
|
|
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
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<string>};
|
2018-11-30 10:37:06 -08:00
|
|
|
|
|
|
|
|
// clang-format off
|
2019-01-16 17:22:53 +00:00
|
|
|
const failedLookup = ts.resolveModuleName(url + '.$ngresource$', fromFile, this.options, this.host) as ResolvedModuleWithFailedLookupLocations;
|
2018-11-30 10:37:06 -08:00
|
|
|
// clang-format on
|
|
|
|
|
if (failedLookup.failedLookupLocations === undefined) {
|
|
|
|
|
throw new Error(
|
2019-01-16 17:22:53 +00:00
|
|
|
`Internal error: expected to find failedLookupLocations during resolution of resource '${url}' in context of ${fromFile}`);
|
2018-11-30 10:37:06 -08:00
|
|
|
}
|
|
|
|
|
|
2019-01-16 17:22:53 +00:00
|
|
|
return failedLookup.failedLookupLocations
|
|
|
|
|
.filter(candidate => candidate.endsWith('.$ngresource$.ts'))
|
|
|
|
|
.map(candidate => candidate.replace(/\.\$ngresource\$\.ts$/, ''));
|
2018-11-30 10:37:06 -08:00
|
|
|
}
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|