/
resource_loader.ts
161 lines (145 loc) Β· 6.06 KB
/
resource_loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* @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';
import * as ts from 'typescript';
import {CompilerHost} from '../transformers/api';
import {ResourceLoader} from './annotations/src/api';
/**
* `ResourceLoader` which delegates to a `CompilerHost` resource loading method.
*/
export class HostResourceLoader implements ResourceLoader {
private cache = new Map<string, string>();
private fetching = new Map<string, Promise<void>>();
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);
}
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;
} else if (this.fetching.has(resolvedUrl)) {
return this.fetching.get(resolvedUrl);
}
const result = this.host.readResource(resolvedUrl);
if (typeof result === 'string') {
this.cache.set(resolvedUrl, result);
return undefined;
} else {
const fetchCompletion = result.then(str => {
this.fetching.delete(resolvedUrl);
this.cache.set(resolvedUrl, str);
});
this.fetching.set(resolvedUrl, fetchCompletion);
return fetchCompletion;
}
}
/**
* 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) !;
}
const result = this.host.readResource ? this.host.readResource(resolvedUrl) :
fs.readFileSync(resolvedUrl, 'utf8');
if (typeof result !== 'string') {
throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`);
}
this.cache.set(resolvedUrl, result);
return result;
}
/**
* 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 (url.startsWith('/')) {
url = url.substr(1);
// Do not take current file location into account if we process absolute path.
fromFile = '';
}
// Turn absolute paths into relative paths.
if (!url.startsWith('.')) {
url = `./${url}`;
}
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>};
// clang-format off
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 '${url}' in context of ${fromFile}`);
}
return failedLookup.failedLookupLocations
.filter(candidate => candidate.endsWith('.$ngresource$.ts'))
.map(candidate => candidate.replace(/\.\$ngresource\$\.ts$/, ''));
}
}