-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathwrite assets.js
549 lines (450 loc) · 15.9 KB
/
write assets.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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
import fs from 'fs-extra'
import path from 'path'
import semver from 'semver'
import require_hacker from 'require-hacker'
import serialize from '../tools/serialize-javascript'
import { exists, clone, replace_all, starts_with, last } from '../helpers'
import { alias_hook, uniform_path, is_relative_path, is_global_path, is_package_path, webpack_uses_tilde_for_node_modules } from '../common'
// one can supply a custom filter
const default_filter = (module, regular_expression) => regular_expression.test(module.name)
// one can supply a custom namer
const default_asset_path = module => module.name
// one can supply a custom parser
const default_parser = module => module.source
// writes webpack-assets.json file, which contains assets' file paths
export default function write_assets(json, options, log)
{
// take the passed in options
options = clone(options)
log.debug(`running write assets webpack plugin v${require('../../package.json').version} with options`, options)
// make webpack stats accessible for asset functions (parser, path, filter)
options.webpack_stats = json
const development = options.development
if (development)
{
log.debug(' (development mode is on)')
}
// write webpack stats json for debugging purpose
if (options.debug)
{
// write webpack stats file
log.debug(`writing webpack stats to ${options.webpack_stats_path}`)
// write the file
// (format the JSON for better readability)
fs.outputFileSync(options.webpack_stats_path, JSON.stringify(json, null, 2))
}
// Note Webpack version due to breaking changes between v2 and v3.
// https://github.com/catamphetamine/webpack-isomorphic-tools/issues/142
//
// Convert things like `3.0.0-alpha.1` to just `3.0.0`.
//
let version = new semver(json.version)
version = `${version.major}.${version.minor}.${version.patch}`
options.webpackVersion = version
options.webpackUsesTildeForNodeModules = webpack_uses_tilde_for_node_modules(version)
// the output object with assets
const output = options.output
output.webpack = { version }
// populate the output object with assets
populate_assets(output, json, options, log)
// write webpack assets info file
if (options.output_to_a_file)
{
// format the JSON for better readability if in debug mode
const assets_info = development ? JSON.stringify(output, null, 2) : JSON.stringify(output)
// rewrite `webpack-assets.json`
let rewrite = true
// for `webpack-assets.json` caching to work
// chunks info should be moved out of it,
// otherwise chunk hashsums constantly change,
// and there won't be any caching.
//
// const assets_buffer = Buffer.from(assets_info)
//
// // if webpack-assets.json already exists,
// // then maybe no need to rewrite it
// if (fs.existsSync(options.webpack_assets_path))
// {
// // previously written webpack-assets.json
// const previous_assets_buffer = fs.readFileSync(options.webpack_assets_path)
//
// // if webpack-assets.json rewrite is not needed, then don't do it
// if (assets_buffer.equals(previous_assets_buffer))
// {
// rewrite = false
// }
// }
// if webpack-assets.json rewrite is needed, then do it
if (rewrite)
{
log.debug(`writing webpack assets info to ${options.webpack_assets_path}`)
// write the file
fs.outputFileSync(options.webpack_assets_path, assets_info)
}
}
else
{
log.debug(`serving webpack assets from memory`)
}
// return Webpack assets JSON object
// for serving it through HTTP service
return output
}
// populates the output object with assets
function populate_assets(output, json, options, log)
{
// for each chunk name ("main", "common", ...)
Object.keys(json.assetsByChunkName).forEach(function(name)
{
log.debug(`getting javascript and styles for chunk "${name}"`)
// get javascript chunk real file path
const javascript = get_assets(name, 'js')[0]
// the second asset is usually a source map
if (javascript)
{
log.debug(` (got javascript)`)
output.javascript[name] = javascript
}
// get style chunk real file path
const style = get_assets(name, 'css')[0]
// the second asset is usually a source map
if (style)
{
log.debug(` (got style)`)
output.styles[name] = style
}
})
// gets asset paths by name and extension of their chunk
function get_assets(name, extension = 'js')
{
let chunk = json.assetsByChunkName[name]
// a chunk could be a string or an array, so make sure it is an array
if (!(Array.isArray(chunk)))
{
chunk = [chunk]
}
return chunk
// filter by extension
.filter(name => path.extname(extract_path(name)) === `.${extension}`)
// adjust the real path (can be http, filesystem)
.map(name => options.assets_base_url + name)
}
// 1st pass
const parsed_assets = {}
// global paths to parsed asset paths
const global_paths_to_parsed_asset_paths = {}
// define __webpack_public_path__ webpack variable
// (resolves "ReferenceError: __webpack_public_path__ is not defined")
const define_webpack_public_path = 'var __webpack_public_path__ = ' + JSON.stringify(options.assets_base_url) + ';\n'
// for each user specified asset type
for (let asset_type of Object.keys(options.assets))
{
// asset type settings
const asset_type_settings = options.assets[asset_type]
// one can supply his own filter
const filter = (asset_type_settings.filter || default_filter) //.bind(this)
// one can supply his own path parser
const extract_asset_path = (asset_type_settings.path || default_asset_path) //.bind(this)
// one can supply his own parser
const parser = (asset_type_settings.parser || default_parser) //.bind(this)
// guard agains typos, etc
// for filter
if (!asset_type_settings.filter)
{
log.debug(`No filter specified for "${asset_type}" assets. Using a default one.`)
}
// for path parser
if (!asset_type_settings.path)
{
log.debug(`No path parser specified for "${asset_type}" assets. Using a default one.`)
}
// for parser
if (!asset_type_settings.parser)
{
log.debug(`No parser specified for "${asset_type}" assets. Using a default one.`)
}
log.debug(`parsing assets of type "${asset_type}"`)
// timer start
const began_at = new Date().getTime()
// get real paths for all the files from this asset type
json.modules
// take just modules of this asset type
.filter(module =>
{
// check that this asset is of the asset type
if (!filter(module, options.regular_expressions[asset_type], options, log))
{
return false
}
// guard against an empty source.
if (!module.source)
{
log.error(`Module "${module.name}" has no source. Maybe Webpack compilation of this module failed. Skipping this asset.`)
return false
}
// include this asset
return true
})
.reduce((set, module) =>
{
// determine asset real path
const asset_path = extract_asset_path(module, options, log)
// asset module source, or asset content (or whatever else)
const parsed_asset = parser(module, options, log)
log.trace(`Adding asset "${asset_path}", module id ${module.id} (in webpack-stats.json)`)
// check for naming collisions (just in case)
if (exists(set[asset_path]))
{
log.error('-----------------------------------------------------------------')
log.error(`Asset with path "${asset_path}" was overwritten because of path collision.`)
log.error(`Use the "filter" function of this asset type to narrow the results.`)
log.error(`Previous asset with this path:`)
log.error(set[asset_path])
log.error(`New asset with this path:`)
log.error(parsed_asset)
log.error('-----------------------------------------------------------------')
}
// add this asset to the list
//
// also resolve "ReferenceError: __webpack_public_path__ is not defined".
// because it may be a url-loaded resource (e.g. a font inside a style).
set[asset_path] = define_webpack_public_path + require_hacker.to_javascript_module_source(parsed_asset)
// add path mapping
global_paths_to_parsed_asset_paths[path.resolve(options.project_path, asset_path)] = asset_path
// continue
return set
},
parsed_assets)
// timer stop
log.debug(` time taken: ${new Date().getTime() - began_at} ms`)
}
// register a special require() hook for requiring() raw webpack modules
const require_hook = require_hacker.global_hook('webpack-module', (required_path, module) =>
{
log.debug(`require()ing "${required_path}"`)
// if Webpack aliases are supplied
if (options.alias)
{
// possibly alias the path
const aliased_global_path = alias_hook(required_path, module, options.project_path, options.alias, log)
// if an alias is found
if (aliased_global_path)
{
const result =
{
source : require_hacker.to_javascript_module_source(safe_require(aliased_global_path, log)),
path : aliased_global_path
}
return result
}
}
// If the `required_path` is an npm package path
// still can't simply `return` and fall back to default Node.js behaviour
// because it could still be an asset like
// `require('bootstrap/style.css')` inside `./style.css`.
//
// If the `required_path` is not an asset though
// (which means "has no extension" in this case — should work)
// then `require()` it as usual.
//
if (required_path.split(path.sep).slice(-1)[0].indexOf('.') === -1)
{
return
}
// find an asset with this path
//
// the require()d path will be global path in case of the for..of require() loop
// for the assets (the code a couple of screens below).
//
// (it can be anything in other cases (e.g. nested require() calls from the assets))
//
if (exists(global_paths_to_parsed_asset_paths[required_path]))
{
log.debug(` found in parsed assets`)
const result =
{
source : parsed_assets[global_paths_to_parsed_asset_paths[required_path]],
path : required_path
}
return result
}
log.debug(` not found in parsed assets, searching in webpack stats`)
// find a webpack module which has a reason with this path
const candidates = []
for (let module of json.modules)
{
for (let reason of module.reasons)
{
if (reason.userRequest === required_path)
{
candidates.push(module)
break
}
}
}
// guard against ambiguity
if (candidates.length === 1)
{
log.debug(` found in webpack stats, module id ${candidates[0].id}`)
// also resolve "ReferenceError: __webpack_public_path__ is not defined".
// because it may be a url-loaded resource (e.g. a font inside a style).
const result =
{
source : define_webpack_public_path + candidates[0].source,
path : candidates[0].identifier
}
return result
}
// if there are more than one candidate for this require()d path,
// then try to guess which one is the one require()d
if (candidates.length > 1)
{
log.debug(` More than a single candidate module was found in webpack stats for require()d path "${required_path}"`)
for (let candidate of candidates)
{
log.debug(' ', candidate)
}
// (loaders matter so the program can't simply throw them away from the required path)
//
// // tries to normalize a cryptic Webpack loader path
// // into a regular relative file path
// // https://webpack.github.io/docs/loaders.html
// let filesystem_required_path = last(required_path
// .replace(/^!!/, '')
// .replace(/^!/, '')
// .replace(/^-!/, '')
// .split('!'))
const fail = () =>
{
throw new Error(`More than a single candidate module was found in webpack stats for require()d path "${required_path}". Enable "debug: true" flag in webpack-isomorphic-tools configuration for more info.`)
}
// https://webpack.github.io/docs/loaders.html
const is_webpack_loader_path = required_path.indexOf('!') >= 0
// if it's a Webpack loader-powered path, the code gives up
if (is_webpack_loader_path)
{
fail()
}
// from here on it's either a filesystem path or an npm module path
// if it's a global path it can be resolved right away
if (is_global_path(required_path))
{
const result =
{
source : require_hacker.to_javascript_module_source(safe_require(required_path, log)),
path : required_path
}
return result
}
// from here on it's either a relative filesystem path or an npm module path,
// so it can be resolved against the require()ing file path (if it can be recovered).
// `module.filename` here can be anything, not just a filesystem absolute path,
// since some advanced require() hook trickery is involved.
// therefore it will be parsed.
//
let requiring_file_path = module.filename.replace(/\.webpack-module$/, '')
// if it's a webpack loader-powered path, then extract the filesystem path from it
if (requiring_file_path.indexOf('!') >= 0)
{
requiring_file_path = requiring_file_path.substring(requiring_file_path.lastIndexOf('!') + 1)
}
// make relative path global
if (is_relative_path(requiring_file_path))
{
requiring_file_path = path.resolve(options.project_path, requiring_file_path)
}
// if `requiring_file_path` is a filesystem path (not an npm module path),
// then the require()d path can possibly be resolved
if (is_global_path(requiring_file_path))
{
log.debug(` The module is being require()d from "${requiring_file_path}", so resolving the path against this file`)
// if it's a relative path, can try to resolve it
if (is_relative_path(required_path))
{
const absolute_path = path.resolve(requiring_file_path, '..', required_path)
const result =
{
source : require_hacker.to_javascript_module_source(safe_require(absolute_path, log)),
path : absolute_path
}
return result
}
// if it's an npm module path (e.g. 'babel-runtime/core-js/object/assign'),
// can try to require() it from the requiring asset path
if (is_package_path(required_path) && is_global_path(module.filename))
{
const absolute_path = require_hacker.resolve(required_path, module)
const result =
{
source : require_hacker.to_javascript_module_source(safe_require(absolute_path, log)),
path : absolute_path
}
return result
}
}
// if it's still here then it means it's either a
fail()
}
})
log.debug(`compiling assets`)
// timer start
const began_at = new Date().getTime()
// evaluate parsed assets source code
for (const asset_path of Object.keys(parsed_assets))
{
let compile = true
// If this asset should be compiled at runtime then skip its compilation.
// (runtime compilation can be used for Webpack loaders returning
// javascript functions or React component classes — stuff like that,
// because it's not serializable to `webpack-assets.json`)
for (const asset_type of Object.keys(options.assets))
{
if (options.regular_expressions[asset_type].test(asset_path))
{
if (options.assets[asset_type].runtime)
{
compile = false
}
}
}
if (!compile)
{
output.assets[asset_path] = parsed_assets[asset_path]
continue
}
// set asset value
log.debug(`compiling asset "${asset_path}"`)
output.assets[asset_path] = safe_require(path.resolve(options.project_path, asset_path), log)
// inside that require() call above
// all the assets are resolved relative to this `module`,
// which is irrelevant because they are all absolute filesystem paths.
//
// if in some of those assets a nested require() call is present
// then it will be resolved relative to that asset folder.
}
// unmount the previously installed require() hook
require_hook.unmount()
// timer stop
log.debug(` time taken: ${new Date().getTime() - began_at} ms`)
}
function safe_require(path, log)
{
try
{
return require(path)
}
catch (error)
{
log.error(error)
return undefined
}
}
export function extract_path(from)
{
const question_mark_index = from.indexOf('?')
if (question_mark_index === -1)
{
return from
}
return from.slice(0, question_mark_index)
}