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';
|
2018-06-26 15:01:09 -07:00
|
|
|
|
|
|
|
|
import {ResourceLoader} from './annotations';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* `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
|
|
|
|
2018-11-30 10:37:06 -08:00
|
|
|
constructor(
|
|
|
|
|
private resolver: (file: string, basePath: string) => string | null,
|
|
|
|
|
private loader: (url: string) => string | Promise<string>) {}
|
2018-06-26 15:01:09 -07:00
|
|
|
|
2018-11-30 10:37:06 -08:00
|
|
|
preload(file: string, containingFile: string): Promise<void>|undefined {
|
|
|
|
|
const resolved = this.resolver(file, containingFile);
|
|
|
|
|
if (resolved === null) {
|
2018-06-26 15:01:09 -07:00
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 10:37:06 -08:00
|
|
|
if (this.cache.has(resolved)) {
|
|
|
|
|
return undefined;
|
|
|
|
|
} else if (this.fetching.has(resolved)) {
|
|
|
|
|
return this.fetching.get(resolved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = this.loader(resolved);
|
2018-06-26 15:01:09 -07:00
|
|
|
if (typeof result === 'string') {
|
2018-11-30 10:37:06 -08:00
|
|
|
this.cache.set(resolved, 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 => {
|
2018-11-30 10:37:06 -08:00
|
|
|
this.fetching.delete(resolved);
|
|
|
|
|
this.cache.set(resolved, str);
|
2018-06-26 15:01:09 -07:00
|
|
|
});
|
2018-11-30 10:37:06 -08:00
|
|
|
this.fetching.set(resolved, fetchCompletion);
|
2018-11-18 22:04:43 +01:00
|
|
|
return fetchCompletion;
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 10:37:06 -08:00
|
|
|
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})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.cache.has(resolved)) {
|
|
|
|
|
return this.cache.get(resolved) !;
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
|
|
|
|
|
2018-11-30 10:37:06 -08:00
|
|
|
const result = this.loader(resolved);
|
2018-06-26 15:01:09 -07:00
|
|
|
if (typeof result !== 'string') {
|
2018-11-30 10:37:06 -08:00
|
|
|
throw new Error(`HostResourceLoader: loader(${resolved}) returned a Promise`);
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|
2018-11-30 10:37:06 -08:00
|
|
|
this.cache.set(resolved, result);
|
2018-06-26 15:01:09 -07:00
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 10:37:06 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// `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-06-26 15:01:09 -07:00
|
|
|
/**
|
|
|
|
|
* `ResourceLoader` which directly uses the filesystem to resolve resources synchronously.
|
|
|
|
|
*/
|
|
|
|
|
export class FileResourceLoader implements ResourceLoader {
|
2018-11-30 10:37:06 -08:00
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
// Strip a leading '/' if one is present.
|
|
|
|
|
if (file.startsWith('/')) {
|
|
|
|
|
file = file.substr(1);
|
|
|
|
|
}
|
|
|
|
|
// Turn absolute paths into relative paths.
|
|
|
|
|
if (!file.startsWith('.')) {
|
|
|
|
|
file = `./${file}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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$';
|
|
|
|
|
|
|
|
|
|
// clang-format off
|
|
|
|
|
const failedLookup = ts.resolveModuleName(file, containingFile, 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}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
2018-06-26 15:01:09 -07:00
|
|
|
}
|