|
1 |
| -var path = require('path'); |
2 |
| -var fs = require('fs'); |
3 |
| -var stripAnsi = require('strip-ansi'); |
4 |
| -var mkdirp = require('mkdirp'); |
5 |
| -var extend = require('deep-extend'); |
6 |
| - |
7 |
| -// @ts-ignore |
8 |
| -var assets = {}; |
9 |
| -var DEFAULT_OUTPUT_FILENAME = 'webpack-stats.json'; |
10 |
| -var DEFAULT_LOG_TIME = false; |
11 |
| - |
12 |
| -function Plugin(options) { |
13 |
| - // @ts-ignore |
14 |
| - this.contents = {}; |
15 |
| - // @ts-ignore |
16 |
| - this.options = options || {}; |
17 |
| - // @ts-ignore |
18 |
| - this.options.filename = this.options.filename || DEFAULT_OUTPUT_FILENAME; |
19 |
| - // @ts-ignore |
20 |
| - if (this.options.logTime === undefined) { |
21 |
| - // @ts-ignore |
22 |
| - this.options.logTime = DEFAULT_LOG_TIME; |
23 |
| - } |
24 |
| -} |
| 1 | +// @ts-check |
| 2 | +/** @typedef {import("lodash.defaults")} defaults */ |
| 3 | +/** @typedef {import("lodash.assign")} assign */ |
| 4 | +/** @typedef {import("lodash.get")} get */ |
| 5 | +/** @typedef {import("webpack").Compiler} Compiler */ |
| 6 | +/** @typedef {import("webpack").Stats} Stats */ |
| 7 | +/** @typedef {import("webpack").compilation.Compilation} Compilation */ |
| 8 | +/** @typedef {import("webpack").compilation.ContextModuleFactory} ContextModuleFactory */ |
| 9 | +/** @typedef {import("webpack").ChunkData} ChunkData */ |
| 10 | +/** @typedef {import("../typings").Contents} Contents */ |
| 11 | +/** @typedef {import("../typings").Options} Options */ |
25 | 12 |
|
26 |
| -Plugin.prototype.apply = function(compiler) { |
27 |
| - var self = this; |
28 |
| - const ouputDirChunk = path.resolve(compiler.options.output.path || process.cwd()); |
29 |
| - const ouputDirTracker = path.resolve(self.options.path || compiler.options.output.path || process.cwd()); |
| 13 | +const path = require('path'); |
| 14 | +const fs = require('fs'); |
| 15 | +const crypto = require('crypto'); |
30 | 16 |
|
31 |
| - // @ts-ignore |
32 |
| - const _compilation = function(compilation, callback) { |
33 |
| - const failedModule = function(fail) { |
34 |
| - var output = { |
35 |
| - status: 'error', |
36 |
| - error: fail.error.name || 'unknown-error', |
37 |
| - }; |
38 |
| - if (fail.error.module !== undefined) { |
39 |
| - output.file = fail.error.module.userRequest; |
40 |
| - } |
41 |
| - if (fail.error.error !== undefined) { |
42 |
| - // @ts-ignore |
43 |
| - output.message = stripAnsi(fail.error.error.codeFrame); |
44 |
| - } else { |
45 |
| - output.message = ''; |
46 |
| - } |
47 |
| - self.writeOutput(compiler, output); |
| 17 | +const defaults = require('lodash.defaults'); |
| 18 | +const assign = require('lodash.assign'); |
| 19 | +const get = require('lodash.get'); |
| 20 | +const stripAnsi = require('strip-ansi'); |
| 21 | + |
| 22 | +class BundleTrackerPlugin { |
| 23 | + /** |
| 24 | + * Track assets file location per bundle |
| 25 | + * @param {Options} options |
| 26 | + */ |
| 27 | + constructor(options) { |
| 28 | + /** @type {Options} */ |
| 29 | + this.options = options; |
| 30 | + /** @type {Contents} */ |
| 31 | + this.contents = { |
| 32 | + status: 'initialization', |
| 33 | + chunks: {}, |
48 | 34 | };
|
| 35 | + this.name = 'BundleTrackerPlugin'; |
| 36 | + |
| 37 | + this.outputChunkDir = ''; |
| 38 | + this.outputTrackerFile = ''; |
| 39 | + this.ouputTrackerDir = ''; |
| 40 | + } |
| 41 | + /** |
| 42 | + * Setup parameter from compiler data |
| 43 | + * @param {Compiler} compiler |
| 44 | + * @returns this |
| 45 | + */ |
| 46 | + _setParamsFromCompiler(compiler) { |
| 47 | + this.options = defaults({}, this.options, { |
| 48 | + path: get(compiler.options, 'output.path', process.cwd()), |
| 49 | + publicPath: get(compiler.options, 'output.publicPath', ''), |
| 50 | + filename: 'webpack-stats.json', |
| 51 | + logTime: false, |
| 52 | + relativePath: false, |
| 53 | + integrity: false, |
| 54 | + // https://www.w3.org/TR/SRI/#cryptographic-hash-functions |
| 55 | + integrityHashes: ['sha256', 'sha384', 'sha512'], |
| 56 | + }); |
| 57 | + |
| 58 | + // Set output directories |
| 59 | + this.outputChunkDir = path.resolve(get(compiler.options, 'output.path', process.cwd())); |
| 60 | + this.outputTrackerFile = path.resolve(path.join(this.options.path, this.options.filename)); |
| 61 | + this.ouputTrackerDir = path.dirname(this.outputTrackerFile); |
| 62 | + |
| 63 | + return this; |
| 64 | + } |
| 65 | + /** |
| 66 | + * Write bundle tracker stats file |
| 67 | + * |
| 68 | + * @param {Compiler} compiler |
| 69 | + * @param {Contents} contents |
| 70 | + */ |
| 71 | + _writeOutput(compiler, contents) { |
| 72 | + assign(this.contents, contents); |
49 | 73 |
|
50 |
| - if (compilation.hooks) { |
51 |
| - const plugin = { name: 'BundleTrackerPlugin' }; |
52 |
| - compilation.hooks.failedModule.tap(plugin, failedModule); |
53 |
| - } else { |
54 |
| - compilation.plugin('failed-module', failedModule); |
| 74 | + if (this.options.publicPath) { |
| 75 | + this.contents.publicPath = this.options.publicPath; |
55 | 76 | }
|
56 |
| - }; |
57 | 77 |
|
58 |
| - // @ts-ignore |
59 |
| - const compile = function(factory, callback) { |
60 |
| - self.writeOutput(compiler, { status: 'compiling' }); |
61 |
| - }; |
| 78 | + fs.mkdirSync(this.ouputTrackerDir, { recursive: true, mode: 0o755 }); |
| 79 | + fs.writeFileSync(this.outputTrackerFile, JSON.stringify(this.contents, null, this.options.indent)); |
| 80 | + } |
| 81 | + /** |
| 82 | + * Compute hash for a content |
| 83 | + * @param {string} content |
| 84 | + */ |
| 85 | + _computeIntegrity(content) { |
| 86 | + return this.options.integrityHashes |
| 87 | + .map(algorithm => { |
| 88 | + const hash = crypto |
| 89 | + .createHash(algorithm) |
| 90 | + .update(content, 'utf8') |
| 91 | + .digest('base64'); |
62 | 92 |
|
63 |
| - const done = function(stats) { |
64 |
| - if (stats.compilation.errors.length > 0) { |
65 |
| - var error = stats.compilation.errors[0]; |
66 |
| - self.writeOutput(compiler, { |
| 93 | + return `${algorithm}-${hash}`; |
| 94 | + }) |
| 95 | + .join(' '); |
| 96 | + } |
| 97 | + /** |
| 98 | + * Handle compile hook |
| 99 | + * @param {Compiler} compiler |
| 100 | + */ |
| 101 | + _handleCompile(compiler) { |
| 102 | + this._writeOutput(compiler, { status: 'compile' }); |
| 103 | + } |
| 104 | + /** |
| 105 | + * Handle compile hook |
| 106 | + * @param {Compiler} compiler |
| 107 | + * @param {Stats} stats |
| 108 | + */ |
| 109 | + _handleDone(compiler, stats) { |
| 110 | + if (stats.hasErrors()) { |
| 111 | + const error = stats.compilation.errors[0]; |
| 112 | + this._writeOutput(compiler, { |
67 | 113 | status: 'error',
|
68 |
| - error: error['name'] || 'unknown-error', |
69 |
| - // @ts-ignore |
| 114 | + error: get(error, 'name', 'unknown-error'), |
70 | 115 | message: stripAnsi(error['message']),
|
71 | 116 | });
|
| 117 | + |
72 | 118 | return;
|
73 | 119 | }
|
74 | 120 |
|
75 |
| - var chunks = {}; |
76 |
| - stats.compilation.chunks.map(function(chunk) { |
77 |
| - var files = chunk.files.map(function(file) { |
78 |
| - var F = { name: file }; |
79 |
| - var publicPath = self.options.publicPath || compiler.options.output.publicPath; |
80 |
| - if (publicPath) { |
81 |
| - F.publicPath = publicPath + file; |
| 121 | + const output = { status: 'done', chunks: {} }; |
| 122 | + stats.compilation.chunkGroups.map(chunkGroup => { |
| 123 | + if (!chunkGroup.isInitial()) return; |
| 124 | + |
| 125 | + output.chunks[chunkGroup.name] = chunkGroup.getFiles().map(filename => { |
| 126 | + const fileInfo = { name: filename }; |
| 127 | + const file = stats.compilation.assets[filename]; |
| 128 | + |
| 129 | + if (this.options.integrity === true) { |
| 130 | + fileInfo.integrity = this._computeIntegrity(file.source()); |
| 131 | + } |
| 132 | + |
| 133 | + if (this.options.publicPath) { |
| 134 | + fileInfo.publicPath = this.options.publicPath + filename; |
82 | 135 | }
|
83 |
| - if (self.options.relativePath === true) { |
84 |
| - const absoluteFilePath = path.join(ouputDirChunk, file); |
85 |
| - F.path = path.relative(ouputDirTracker, absoluteFilePath); |
| 136 | + |
| 137 | + if (this.options.relativePath === true) { |
| 138 | + const absoluteFilePath = path.join(this.outputChunkDir, filename); |
| 139 | + fileInfo.path = path.relative(this.ouputTrackerDir, absoluteFilePath); |
86 | 140 | } else {
|
87 |
| - F.path = path.join(ouputDirChunk, file); |
| 141 | + fileInfo.path = path.join(this.outputChunkDir, filename); |
88 | 142 | }
|
89 | 143 |
|
90 |
| - return F; |
| 144 | + return fileInfo; |
91 | 145 | });
|
92 |
| - chunks[chunk.name] = files; |
93 | 146 | });
|
94 |
| - var output = { |
95 |
| - status: 'done', |
96 |
| - chunks: chunks, |
97 |
| - }; |
98 | 147 |
|
99 |
| - if (self.options.logTime === true) { |
| 148 | + if (this.options.logTime === true) { |
100 | 149 | output.startTime = stats.startTime;
|
101 | 150 | output.endTime = stats.endTime;
|
102 | 151 | }
|
103 | 152 |
|
104 |
| - self.writeOutput(compiler, output); |
105 |
| - }; |
106 |
| - |
107 |
| - if (compiler.hooks) { |
108 |
| - const plugin = { name: 'BundleTrackerPlugin' }; |
109 |
| - compiler.hooks.compilation.tap(plugin, _compilation); |
110 |
| - compiler.hooks.compile.tap(plugin, compile); |
111 |
| - compiler.hooks.done.tap(plugin, done); |
112 |
| - } else { |
113 |
| - compiler.plugin('compilation', _compilation); |
114 |
| - compiler.plugin('compile', compile); |
115 |
| - compiler.plugin('done', done); |
116 |
| - } |
117 |
| -}; |
118 |
| - |
119 |
| -Plugin.prototype.writeOutput = function(compiler, contents) { |
120 |
| - var outputDir = this.options.path || '.'; |
121 |
| - var outputFilename = path.join(outputDir, this.options.filename || DEFAULT_OUTPUT_FILENAME); |
122 |
| - var publicPath = this.options.publicPath || compiler.options.output.publicPath; |
123 |
| - if (publicPath) { |
124 |
| - contents.publicPath = publicPath; |
| 153 | + this._writeOutput(compiler, output); |
125 | 154 | }
|
126 |
| - mkdirp.sync(path.dirname(outputFilename)); |
| 155 | + /** |
| 156 | + * Method called by webpack to apply plugin hook |
| 157 | + * @param {Compiler} compiler |
| 158 | + */ |
| 159 | + apply(compiler) { |
| 160 | + this._setParamsFromCompiler(compiler); |
127 | 161 |
|
128 |
| - this.contents = extend(this.contents, contents); |
129 |
| - // @ts-ignore |
130 |
| - fs.writeFileSync(outputFilename, JSON.stringify(this.contents, null, this.options.indent)); |
131 |
| -}; |
| 162 | + compiler.hooks.compile.tap(this.name, this._handleCompile.bind(this, compiler)); |
| 163 | + compiler.hooks.done.tap(this.name, this._handleDone.bind(this, compiler)); |
| 164 | + } |
| 165 | +} |
132 | 166 |
|
133 |
| -module.exports = Plugin; |
| 167 | +module.exports = BundleTrackerPlugin; |
0 commit comments