-
-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathResolver.js
More file actions
320 lines (268 loc) · 9.44 KB
/
Resolver.js
File metadata and controls
320 lines (268 loc) · 9.44 KB
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
const fs = require('fs');
const path = require('path');
const Collection = require('./Collection');
const { resolveException } = require('./Messages/Exception');
const Snapshot = require('./Snapshot');
class Resolver {
fs = null;
/**
* @type {string} The issuer filename of required the file.
*/
issuerFile = '';
/**
* @type {FileInfo} The issuer of required the file.
*/
issuer = {};
/**
* @type {AssetEntryOptions} The current entry point where are resolved all dependencies.
*/
entryPoint = {};
/**
* @type {string} The context directory of required the file.
*/
context = '';
/**
* The cache of resolved source files. Defined at one time.
*
* @type {Map<string, Map<string, string>>}
*/
sourceFiles = new Map();
/**
* The data of assets to resolving output assets.
* For each new chunk must be cleaned.
* Note: the same module can have many issuers and can be saved under different asset filenames.
*
* @type {Map<string, {assets:Map, originalFilename?:string, resolve?:(issuer:FileInfo) => string}>}
*/
data = new Map();
pluginOption = null;
collection = null;
assetEntry = null;
assetInline = null;
constructor({ pluginOption, assetEntry, assetInline, collection }) {
this.pluginOption = pluginOption;
this.collection = collection;
this.assetEntry = assetEntry;
this.assetInline = assetInline;
}
/**
* @param {FileSystem} fs
*/
init({ fs }) {
this.fs = fs;
// webpack root context path
this.rootContext = this.pluginOption.context;
// bind this context to the method for using in VM context
this.require = this.require.bind(this);
}
/**
* Set the current issuer and the entry point.
*
* @param {Object} entryPoint The current entry point.
* @param {FileInfo} issuer The issuer.
*/
setContext(entryPoint, issuer) {
const [file] = issuer.resource.split('?', 1);
this.context = path.dirname(file);
this.entryPoint = entryPoint;
this.issuer = issuer;
this.issuerFile = file;
}
/**
* Add the context and resolved path of the resource to resolve it in require() at render time.
*
* @param {{resource: string, filename: string, resolve?: (FileInfo)=> string}} assetInfo The asset info.
*/
addAsset(assetInfo) {
let sourceFile = path.resolve(assetInfo.resource);
let resourceData = this.data.get(sourceFile);
if (!resourceData) {
resourceData = {
assets: new Map(),
sourceFile,
};
this.data.set(sourceFile, resourceData);
}
if (assetInfo.resolve) {
resourceData.resolve = assetInfo.resolve;
} else {
resourceData.originalFilename = assetInfo.filename;
}
}
/**
* Resolve the full path of asset source file.
*
* @param {string} rawRequest The raw request of resource.
* @param {string} issuer The issuer of resource.
* @return {string|null} The resolved full path of resource.
*/
resolveResource(rawRequest, issuer) {
let resource = this.sourceFiles.get(issuer)?.get(rawRequest);
if (resource) return resource;
// normalize request, e.g., the relative `path/to/../to/file` path to absolute `path/to/file`
resource = path.resolve(this.context, rawRequest);
const [file] = resource.split('?', 1);
if (rawRequest.startsWith(this.context) || this.fs.existsSync(file)) {
this.addSourceFile(resource, rawRequest, issuer);
return resource;
}
return null;
}
/**
* Add resolved source file to data.
*
* @param {string} sourceFile The resolved full path of resource.
* @param {string} rawRequest The rawRequest of resource.
* @param {string} issuer The issuer of resource.
*/
addSourceFile(sourceFile, rawRequest, issuer) {
let item = this.sourceFiles.get(issuer);
if (item == null) {
this.sourceFiles.set(issuer, new Map([[rawRequest, sourceFile]]));
} else {
item.set(rawRequest, sourceFile);
}
}
/**
* Get the key of asset file to save it as resolved under its issuer.
*
* Note: the key can be an output filename when the issuer is a current entry point
* otherwise is a source file of an issuer, e.g., a style.
*
* @param {string} issuer
* @param {AssetEntryOptions} entryPoint
*/
getAssetKey(issuer, entryPoint) {
return issuer === entryPoint?.resource ? entryPoint.filename : issuer;
}
/**
* Resolve output filename, given the auto public path.
* Resolves styles, images, fonts and others except scripts.
*
* @param {string} resource The resource file, including a query.
* @return {string|null}
*/
resolveAsset(resource) {
const resourceData = this.data.get(resource);
if (!resourceData) return null;
const { entryPoint, issuer } = this;
const assetKey = this.getAssetKey(issuer.resource, entryPoint);
const assetFile = resourceData.assets.get(assetKey);
const isIssuerInlinedStyle = this.collection.isInlineStyle(issuer.resource);
if (assetFile && !isIssuerInlinedStyle) return assetFile;
const { originalFilename, resolve } = resourceData;
const realIssuer = isIssuerInlinedStyle
? {
resource: entryPoint.resource,
filename: entryPoint.filename,
}
: issuer;
let outputFilename;
if (originalFilename != null) {
outputFilename = this.pluginOption.getOutputFilename(originalFilename, realIssuer.filename);
} else if (resolve != null) {
// resolve asset filename processed via external loader, e.g. `responsive-loader`
outputFilename = resolve(realIssuer);
}
if (outputFilename != null) {
resourceData.assets.set(assetKey, outputFilename);
if (this.collection.hasStyle(resource)) {
// set the output filename for already created (in renderManifest) data
this.collection.setResourceFilename(this.entryPoint, { resource, filename: outputFilename });
} else {
this.collection.setData(this.entryPoint, issuer, {
type: Collection.type.resource,
inline: false,
resource,
assetFile: outputFilename,
});
}
}
return outputFilename;
}
/**
* Require the resource request in the compiled template or CSS.
*
* @param {string} rawRequest The raw request of source resource.
* @returns {string} The output asset filename generated by filename template.
* @throws {Error}
*/
require(rawRequest) {
const { issuer, issuerFile } = this;
// @import CSS rule is not supported
if (rawRequest.includes('??ruleSet')) {
resolveException(rawRequest, issuer.resource, this.rootContext, this.pluginOption);
}
// bypass the asset contained data-URL
if (this.assetInline.isDataUrl(rawRequest)) return rawRequest;
// bypass the source script file to replace it after the process
if (this.collection.hasScript(rawRequest)) return rawRequest;
// bypass the inline CSS
if (this.collection.isInlineStyle(rawRequest)) return rawRequest;
const resource = this.resolveResource(rawRequest, issuerFile);
// resolve resource
if (resource != null) {
// bypass the asset/inline as inline SVG
if (
this.assetInline.isSvgFile(resource) &&
this.pluginOption.isEntry(issuer.resource) &&
this.pluginOption.isEmbedSvg(resource, issuer.resource)
) {
this.collection.setData(this.entryPoint, issuer, {
type: Collection.type.inlineSvg,
inline: true, // embed into DOM by replacing <img> with <svg>
resource,
});
return resource;
}
const assetFile = this.resolveAsset(resource);
if (assetFile != null) return assetFile;
// try to resolve inline data url
const dataUrl = this.assetInline.getDataUrl(resource, issuerFile);
if (dataUrl != null) {
this.collection.setData(this.entryPoint, issuer, {
type: Collection.type.resource,
inline: true,
resource,
});
return dataUrl;
}
}
// if is used the filename like `./main.js`, then the resource is an absolute file
// if is used the filename like `../js/main.js`, then the resource is null and the rawRequest is an absolute file
const file = resource || rawRequest;
if (this.pluginOption.js.test.test(file) && this.assetEntry.isEntryResource(issuer.resource)) {
const [sourceFile] = file.split('?', 1);
if (
this.fs.existsSync(sourceFile) &&
this.collection.recoverMissingScript({ resource: file, issuer, entry: this.entryPoint })
) {
return file;
}
// occur after rename/delete of a js file when the entry module was already rebuilt
Snapshot.addMissingFile(issuer.resource, file);
resolveException(file, issuer.resource, this.rootContext, this.pluginOption);
}
// require a native JavaScript, JSON or css-loader API file
if (/\.js[a-z0-9]*$/i.test(resource)) return require(resource);
resolveException(rawRequest, issuer.resource, this.rootContext, this.pluginOption);
}
/**
* Clear caches.
* Called only once when the plugin is applied.
*/
clear() {
this.data.clear();
this.sourceFiles.clear();
}
/**
* Reset settings.
* Called before each new compilation after changes, in the serve/watch mode.
*/
reset() {
// reset outdated assets after rebuild via webpack dev server
// note: new filenames are generated on the fly in the this.resolveAsset() method
this.data.forEach((item) => item.assets.clear());
}
}
module.exports = Resolver;