-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
Resolver.js
320 lines (271 loc) · 9.78 KB
/
Resolver.js
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 path = require('path');
const Asset = require('./Asset');
const AssetInline = require('./AssetInline');
const AssetScript = require('./AssetScript');
const AssetSource = require('./AssetSource');
const { resolveException, duplicateScriptWarning, duplicateStyleWarning } = require('./Exceptions');
class Resolver {
static fs = null;
/**
* @type {string} The issuer filename of required the file.
*/
static issuerFile = '';
/**
* @type {string} The issuer request of required the file.
*/
static issuerRequest = '';
/**
* @type {string} The output filename of current entry point.
*/
static entryAsset = '';
/**
* @type {string} The context directory of required the file.
*/
static context = '';
/**
* The cache of resolved source files. Defined at one time.
*
* @type {Map<string, Map<string, string>>}
*/
static sourceFiles = new Map();
/**
* The data of assets sources and issuers. Used for resolving output assets.
* For each new chunk must be cleaned.
* Note: same module can have many issuers and can be saved under different asset filenames.
*
* @type {Map<string, {issuers:Map, originalAssetFile:string, moduleHandler?:Function<originalAssetFile:string, issuer:string>}>}
*/
static data = new Map();
/**
* The cache of duplicate scripts and styles.
*/
static duplicates = new Map();
/**
* @param {FileSystem} fs
* @param {string} rootContext The Webpack root context path.
*/
static init({ fs, rootContext }) {
this.fs = fs;
this.rootContext = rootContext;
// bind this context to the method for using in VM context
this.require = this.require.bind(this);
}
/**
* Clear caches.
* This method is called only once, when the plugin is applied.
*/
static clear() {
this.data.clear();
this.sourceFiles.clear();
}
/**
* Reset settings.
* This method is called before each compilation after changes by `webpack serv/watch`.
*/
static 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.issuers.clear());
this.duplicates.clear();
}
/**
* Set the current source file, which is the issuer for assets when compiling the source code.
*
* @param {string} issuer The issuer request.
* @param {string} entryAsset The current entry point.
*/
static setIssuer(issuer, entryAsset) {
const [file] = issuer.split('?', 1);
this.issuerFile = file;
this.issuerRequest = issuer;
this.context = path.dirname(file);
this.entryAsset = entryAsset;
}
/**
* Add the context and resolved path of the resource to resolve it in require() at render time.
*
* @param {string} sourceFile The full path of source asset file.
* @param {string} assetFile The original asset filename generated by Webpack, relative by output path.
* @param {string} issuer The issuer of asset file.
*/
static addAsset(sourceFile, assetFile, issuer) {
sourceFile = path.resolve(sourceFile);
let item = this.data.get(sourceFile);
if (!item) {
this.data.set(sourceFile, {
issuers: new Map([[issuer, undefined]]),
originalAssetFile: assetFile,
});
return;
}
// don't override already resolved assets
if (!item.issuers.has(issuer)) {
item.issuers.set(issuer, undefined);
}
if (assetFile != null) item.originalAssetFile = assetFile;
}
/**
* Add the resolved output asset file.
*
* @param {string} sourceFile The full path of source asset file.
* @param {string} assetFile The resolved output asset filename, given the auto public path.
* @param {string} issuer The issuer of asset file.
*/
static addResolvedAsset(sourceFile, assetFile, issuer) {
sourceFile = path.resolve(sourceFile);
let item = this.data.get(sourceFile);
if (!item) {
this.data.set(sourceFile, {
issuers: new Map([[issuer, assetFile]]),
});
return;
}
item.issuers.set(issuer, assetFile);
}
/**
* @param {string} sourceFile The full path of source asset file.
* @param {Object} moduleHandler External handler for processing of the asset module.
*/
static setModuleHandler(sourceFile, moduleHandler = null) {
let item = this.data.get(sourceFile);
if (item) {
item.moduleHandler = moduleHandler;
}
}
/**
* Resolve full path of asset source file by raw request and issuer.
*
* @param {string} rawRequest The raw request of resource.
* @param {string} issuer The issuer of resource.
* @return {string|null} The resolved full path of resource.
*/
static getSourceFile(rawRequest, issuer) {
let sourceFile = this.sourceFiles.get(issuer)?.get(rawRequest);
if (sourceFile) return sourceFile;
// normalize request, e.g. the relative `path/to/../to/file` path to absolute `path/to/file`
sourceFile = path.resolve(this.context, rawRequest);
const [file] = sourceFile.split('?', 1);
if (rawRequest.startsWith(this.context) || this.fs.existsSync(file)) {
this.addSourceFile(sourceFile, rawRequest, issuer);
return sourceFile;
}
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.
*/
static 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);
}
}
/**
* Whether in issuer used duplicate script or style.
* Note: using duplicate scripts in the same Pug file doesn't make sense, must be used only one file.
*
* @param {string} file
* @param {string} issuer
* @return {boolean}
*/
static isDuplicate(file, issuer) {
if (!this.duplicates.has(issuer)) {
this.duplicates.set(issuer, new Set([file]));
return false;
}
const duplicate = this.duplicates.get(issuer);
if (duplicate.has(file)) return true;
duplicate.add(file);
return false;
}
/**
* Resolve output asset filename, given the auto public path.
*
* @param {string} sourceFile The resolved full path of resource.
* @param {string} issuer The issuer of resource.
* @param {string|null} entryAsset
* @return {string|null}
*/
static resolveAsset(sourceFile, issuer, entryAsset) {
const item = this.data.get(sourceFile);
if (!item) return null;
let assetFile = item.issuers.get(issuer);
if (assetFile && !entryAsset) return assetFile;
const { originalAssetFile, moduleHandler } = item;
let assetOutputFile;
if (originalAssetFile != null) {
// normalize output asset files
if (AssetInline.isDataUrl(originalAssetFile)) {
assetOutputFile = originalAssetFile;
} else {
const issuerAssetFile = entryAsset || Asset.findAssetFile(issuer);
if (issuerAssetFile) {
assetOutputFile = Asset.getOutputFile(originalAssetFile, issuerAssetFile);
}
}
} else if (moduleHandler != null) {
// normalize output asset files processed via external loader, e.g. `responsive-loader`
assetOutputFile = moduleHandler(originalAssetFile, issuer);
}
if (assetOutputFile != null) {
item.issuers.set(issuer, assetOutputFile);
}
return assetOutputFile;
}
/**
* Require the resource request in the compiled pug or css.
*
* @param {string} rawRequest The raw request of source resource.
* @returns {string} The output asset filename generated by filename template.
* @throws {Error}
*/
static require(rawRequest) {
const { entryAsset, issuerFile, issuerRequest, context } = this;
const request = path.resolve(context, rawRequest);
// @import CSS rule is not supported
if (rawRequest.indexOf('??ruleSet') > 0) resolveException(rawRequest, issuerRequest);
// require script in tag <script src=require('./main.js')>, set an asset filename via replaceSourceFilesInCompilation()
const scriptFile = AssetScript.resolveFile(rawRequest);
if (scriptFile != null) {
if (this.isDuplicate(scriptFile, issuerRequest)) {
const filePath = path.relative(this.rootContext, scriptFile);
const issuerPath = path.relative(this.rootContext, issuerRequest);
duplicateScriptWarning(filePath, issuerPath);
}
return scriptFile;
}
// bypass the asset contained data-URL
if (AssetInline.isDataUrl(rawRequest)) return rawRequest;
// bypass the inline CSS
if (AssetSource.isInline(rawRequest)) return rawRequest;
// bypass the asset/inline as inline SVG
if (AssetInline.isInlineSvg(request, issuerFile)) return request;
// resolve resources
const sourceFile = this.getSourceFile(rawRequest, issuerFile);
if (sourceFile != null) {
const isInline = AssetSource.isInline(issuerRequest);
const assetFile = this.resolveAsset(sourceFile, issuerRequest, isInline ? entryAsset : null);
if (assetFile != null) {
if (assetFile.endsWith('.css') && this.isDuplicate(assetFile, issuerRequest)) {
const filePath = path.relative(this.rootContext, sourceFile);
const issuerPath = path.relative(this.rootContext, issuerRequest);
duplicateStyleWarning(filePath, issuerPath);
}
return assetFile;
}
// try to resolve inline data url
const dataUrl = AssetInline.getDataUrl(sourceFile, issuerFile);
if (dataUrl != null) return dataUrl;
}
// require only js code or json data
if (/\.js[a-z0-9]*$/i.test(rawRequest)) return require(request);
resolveException(rawRequest, issuerRequest);
}
}
module.exports = Resolver;