UNPKG

7.76 kBJavaScriptView Raw
1const crypto = require('crypto');
2const webpack = require('webpack');
3const path = require('path');
4const { expect } = require('chai');
5const rimraf = require('rimraf');
6const fs = require('fs');
7
8// Each compilation may use a different hash fn, so we need to generate one from the webpack
9// outputOptions.
10const makeHashFn = ({
11 hashFunction = 'md5',
12 hashDigest = 'hex',
13 hashDigestLength = 20,
14 hashSalt = null,
15} = {}) => (input) => {
16 const hashObj = crypto.createHash(hashFunction).update(input);
17 if (hashSalt) hashObj.update(hashSalt);
18 const fullHash = hashObj.digest(hashDigest);
19 return { fullHash, shortHash: fullHash.substr(0, hashDigestLength) };
20};
21
22
23const expectAssetsNameToContainHash = (stats, filter = () => true) => {
24 const { assets, outputOptions } = stats.compilation;
25 const hashFn = makeHashFn(outputOptions);
26 expect(Object.keys(assets)).to.have.length.at.least(1);
27 Object.keys(assets).filter(filter).forEach((name) => {
28 const asset = assets[name];
29 const { shortHash } = hashFn(asset.source());
30 expect(name).to.contain(shortHash);
31 });
32};
33
34const webpackCompile = (fixture, mode) => new Promise((resolve, reject) => {
35 const dir = path.resolve(__dirname, fixture);
36 const config = path.resolve(dir, 'webpack.config.js');
37 // eslint-disable-next-line global-require
38 const opts = Object.assign(require(config), { mode, context: dir });
39 webpack(opts, (err, stats) => {
40 if (err) reject(err);
41 else resolve(stats);
42 });
43});
44
45const findAssetByName = (assets, name) => {
46 const assetName = Object.keys(assets).map(n => n.split('.')).find(n => n[0] === name).join('.');
47 return assets[assetName];
48};
49
50const extractHashes = (assets, filter) => Object.keys(assets)
51 .map(n => n.split('.'))
52 .filter(filter)
53 .map(n => n[1]);
54
55describe('OutputHash', () => {
56 const modes = ['development', 'production'];
57
58 before(() => {
59 if (fs.existsSync('./test/tmp')) {
60 rimraf.sync('./test/tmp');
61 }
62 });
63
64 modes.forEach((mode) => {
65 context(`In ${mode} mode`, () => {
66 it('Works with single entry points', () => webpackCompile('one-asset', mode)
67 .then((stats) => {
68 expectAssetsNameToContainHash(stats);
69 }));
70
71 it('Works with hashSalt', () => webpackCompile('one-asset-salt', mode)
72 .then((stats) => {
73 expectAssetsNameToContainHash(stats);
74 }));
75
76 it('Works with hashFunction (sha256)', () => webpackCompile('one-asset-sha256', mode)
77 .then((stats) => {
78 expectAssetsNameToContainHash(stats);
79 }));
80
81 it('Works with hashDigest (base64)', () => webpackCompile('one-asset-base64', mode)
82 .then((stats) => {
83 expectAssetsNameToContainHash(stats);
84 }));
85
86 it('Works with multiple entry points', () => webpackCompile('multi-asset', mode)
87 .then((stats) => {
88 expectAssetsNameToContainHash(stats);
89 }));
90
91 it('Works with manifest file', () => webpackCompile('manifest', mode)
92 .then((stats) => {
93 expectAssetsNameToContainHash(stats);
94
95 const { compilation: { entrypoints, assets } } = stats;
96 const entryPointNames = Array.from(entrypoints.keys());
97
98 // Find all the async required assets (i.e. not manifest or entry point chunks)
99 const hashes = extractHashes(assets, n => n[0] !== 'manifest'
100 && entryPointNames.indexOf(n[0]) === -1);
101 const commons = findAssetByName(assets, 'manifest');
102
103 // We expect 1 entry chunk (async.js)
104 expect(hashes).to.have.lengthOf(1);
105 hashes.forEach((hash) => {
106 expect(commons.source()).to.contain(hash);
107 });
108 }));
109
110 it('Works with HTML output', () => webpackCompile('html', mode)
111 .then((stats) => {
112 expectAssetsNameToContainHash(stats, name => name !== 'index.html');
113
114 const hashes = extractHashes(stats.compilation.assets, n => n[0] !== 'index');
115 const commons = findAssetByName(stats.compilation.assets, 'index');
116
117 expect(hashes).to.have.lengthOf(2);
118 hashes.forEach((hash) => {
119 expect(commons.source()).to.contain(hash);
120 });
121 }));
122
123 it('Works with code splitting', () => webpackCompile('code-split', mode)
124 .then((stats) => {
125 const main = findAssetByName(stats.compilation.assets, 'main');
126 const asyncChunk = stats.compilation.chunks.filter(c => c.name === null)[0];
127
128 expectAssetsNameToContainHash(stats);
129 expect(main.source()).to.contain(asyncChunk.renderedHash);
130 }));
131
132
133 it('Works with sourcemaps', () => webpackCompile('sourcemap', mode)
134 .then((stats) => {
135 const { compilation: { entrypoints, assets } } = stats;
136 const entryPointNames = Array.from(entrypoints.keys());
137
138 // Check the hash is valid for all non-source map files
139 expectAssetsNameToContainHash(stats, asset => asset.indexOf('.map') === -1);
140
141 entryPointNames.forEach((entryPoint) => {
142 const entryPointAssets = Object.keys(assets)
143 .filter(key => key.includes(entryPoint));
144
145 const sourceMap = entryPointAssets
146 .find(assetName => assetName.indexOf('.map') !== -1);
147
148 const assetKey = entryPointAssets
149 .find(assetName => assetName.indexOf('.map') === -1);
150
151 // We expect an asset file along with a single source map
152 expect(entryPointAssets.length).to.equal(2);
153
154 // Source code still points to the old sourcemap
155 expect(assets[assetKey].source()).to.contain(`sourceMappingURL=${sourceMap}`);
156
157 // But sourcemaps has the name of the new source code file
158 expect(assets[sourceMap].source()).to.contain(assetKey);
159 });
160 }));
161
162 it('Works with runtime chunks', () => webpackCompile('runtime-chunks', mode)
163 .then((stats) => {
164 const asyncChunk = stats.compilation.chunks.filter(c => c.name === null)[0];
165 const runtime1 = findAssetByName(stats.compilation.assets, 'runtime~entry1');
166 const runtime2 = findAssetByName(stats.compilation.assets, 'runtime~entry2');
167
168 expectAssetsNameToContainHash(stats);
169 expect(runtime1.source()).to.contain(asyncChunk.renderedHash);
170 expect(runtime2.source()).to.contain(asyncChunk.renderedHash);
171 }));
172
173 it('Works with async loops', () => webpackCompile('loop')
174 .then((stats) => {
175 const entry = findAssetByName(stats.compilation.assets, 'entry');
176 const asyncChunk = stats.compilation.chunks.filter(c => c.name === null)[0];
177
178 expectAssetsNameToContainHash(stats);
179 expect(entry.source()).to.contain(asyncChunk.renderedHash);
180 }));
181 });
182 });
183});