diff --git a/README.md b/README.md index 74354a34..0f799550 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,11 @@ Some further configuration is possible through provided plugins. ```js plugins: [ new HardSourceWebpackPlugin(), +``` + +### ExcludeModulePlugin +```js // You can optionally exclude items that may not be working with HardSource // or items with custom loaders while you are actively developing the // loader. @@ -80,6 +84,30 @@ Some further configuration is possible through provided plugins. include: path.join(__dirname, 'vendor'), }, ]), +``` + +### ParallelModulePlugin + +```js + // HardSource includes an experimental plugin for parallelizing webpack + // across multiple processes. It requires that the extra processes have the + // same configuration. `mode` must be set in the config. Making standard + // use with webpack-dev-server or webpack-serve is difficult. Using it with + // webpack-dev-server or webpack-serve means disabling any automatic + // configuration and configuring hot module replacement support manually. + new HardSourceWebpackPlugin.ParallelModulePlugin({ + // How to launch the extra processes. Default: + fork: (fork, compiler, webpackBin) => fork( + webpackBin(), + ['--config', __filename], { + silent: true, + } + ), + // Number of workers to spawn. Default: + numWorkers: () => require('os').cpus().length, + // Number of modules built before launching parallel building. Default: + minModules: 10, + }), ] ``` diff --git a/index.js b/index.js index 7de4d282..3d415def 100644 --- a/index.js +++ b/index.js @@ -577,3 +577,9 @@ HardSourceWebpackPlugin.SerializerAppend2Plugin = SerializerAppend2Plugin; HardSourceWebpackPlugin.SerializerAppendPlugin = SerializerAppendPlugin; HardSourceWebpackPlugin.SerializerCacachePlugin = SerializerCacachePlugin; HardSourceWebpackPlugin.SerializerJsonPlugin = SerializerJsonPlugin; + +Object.defineProperty(HardSourceWebpackPlugin, 'ParallelModulePlugin', { + get() { + return require('./lib/ParallelModulePlugin'); + }, +}); diff --git a/lib/ParallelLauncherPlugin.js b/lib/ParallelLauncherPlugin.js new file mode 100644 index 00000000..e69de29b diff --git a/lib/ParallelModulePlugin.js b/lib/ParallelModulePlugin.js new file mode 100644 index 00000000..9f957216 --- /dev/null +++ b/lib/ParallelModulePlugin.js @@ -0,0 +1,306 @@ +const { fork: cpFork } = require('child_process'); +const { cpus } = require('os'); +const { resolve } = require('path'); + +const logMessages = require('./util/log-messages'); +const pluginCompat = require('./util/plugin-compat'); + +const webpackBin = () => { + try { + return require.resolve('webpack-cli'); + } catch (e) {} + try { + return require.resolve('webpack-command'); + } catch (e) {} + throw new Error('webpack cli tool not installed or discoverable'); +}; + +const configPath = compiler => { + try { + return require.resolve( + resolve(compiler.options.context || process.cwd(), 'webpack.config'), + ); + } catch (e) {} + try { + return require.resolve(resolve(process.cwd(), 'webpack.config')); + } catch (e) {} + throw new Error('config not in obvious location'); +}; + +class ParallelModulePlugin { + constructor(options) { + this.options = options; + } + + apply(compiler) { + try { + require('webpack/lib/JavascriptGenerator'); + } catch (e) { + logMessages.parallelRequireWebpack4(compiler); + return; + } + + const options = this.options || {}; + const fork = + options.fork || + ((fork, compiler, webpackBin) => + fork(webpackBin(compiler), ['--config', configPath(compiler)], { + silent: true, + })); + const numWorkers = options.numWorkers + ? typeof options.numWorkers === 'function' + ? options.numWorkers + : () => options.numWorkers + : () => cpus().length; + const minModules = + typeof options.minModules === 'number' ? options.minModules : 10; + + const compilerHooks = pluginCompat.hooks(compiler); + + let freeze, thaw; + + compilerHooks._hardSourceMethods.tap('ParallelModulePlugin', methods => { + freeze = methods.freeze; + thaw = methods.thaw; + }); + + compilerHooks.thisCompilation.tap( + 'ParallelModulePlugin', + (compilation, params) => { + const compilationHooks = pluginCompat.hooks(compilation); + const nmfHooks = pluginCompat.hooks(params.normalModuleFactory); + + const doMaster = () => { + const jobs = {}; + const readyJobs = {}; + const workers = []; + + let nextWorkerIndex = 0; + + let start = 0; + let started = false; + let configMismatch = false; + + let modules = 0; + + const startWorkers = () => { + const _numWorkers = numWorkers(); + logMessages.parallelStartWorkers(compiler, { + numWorkers: _numWorkers, + }); + + for (let i = 0; i < _numWorkers; i++) { + const worker = fork(cpFork, compiler, webpackBin); + workers.push(worker); + worker.on('message', _result => { + if (configMismatch) { + return; + } + + if (_result.startsWith('ready:')) { + const configHash = _result.split(':')[1]; + if (configHash !== compiler.__hardSource_configHash) { + logMessages.parallelConfigMismatch(compiler, { + outHash: compiler.__hardSource_configHash, + theirHash: configHash, + }); + + configMismatch = true; + killWorkers(); + for (const id in jobs) { + jobs[id].cb({ error: true }); + delete readyJobs[id]; + delete jobs[id]; + } + return; + } + } + + if (Object.values(readyJobs).length) { + const id = Object.keys(readyJobs)[0]; + worker.send( + JSON.stringify({ + id, + data: readyJobs[id].data, + }), + ); + delete readyJobs[id]; + } else { + worker.ready = true; + } + + if (_result.startsWith('ready:')) { + start = Date.now(); + return; + } + + const result = JSON.parse(_result); + jobs[result.id].cb(result); + delete [result.id]; + }); + } + }; + + const killWorkers = () => { + Object.values(workers).forEach(worker => worker.kill()); + }; + + const doJob = (module, cb) => { + if (configMismatch) { + cb({ error: new Error('config mismatch') }); + return; + } + + const id = 'xxxxxxxx-xxxxxxxx'.replace(/x/g, () => + Math.random() + .toString(16) + .substring(2, 3), + ); + jobs[id] = { + id, + data: freeze('Module', null, module, { + id: module.identifier(), + compilation, + }), + cb, + }; + + const worker = Object.values(workers).find(worker => worker.ready); + if (worker) { + worker.ready = false; + worker.send( + JSON.stringify({ + id, + data: jobs[id].data, + }), + ); + } else { + readyJobs[id] = jobs[id]; + } + + if (!started) { + started = true; + startWorkers(); + } + }; + + const _create = params.normalModuleFactory.create; + params.normalModuleFactory.create = (data, cb) => { + _create.call(params.normalModuleFactory, data, (err, module) => { + if (err) { + return cb(err); + } + if (module.constructor.name === 'NormalModule') { + const build = module.build; + module.build = ( + options, + compilation, + resolver, + fs, + callback, + ) => { + if (modules < minModules) { + build.call( + module, + options, + compilation, + resolver, + fs, + callback, + ); + modules += 1; + return; + } + + try { + doJob(module, result => { + if (result.error) { + build.call( + module, + options, + compilation, + resolver, + fs, + callback, + ); + } else { + thaw('Module', module, result.module, { + compilation, + normalModuleFactory: params.normalModuleFactory, + contextModuleFactory: params.contextModuleFactory, + }); + callback(); + } + }); + } catch (e) { + logMessages.parallelErrorSendingJob(compiler, e); + build.call( + module, + options, + compilation, + resolver, + fs, + callback, + ); + } + }; + cb(null, module); + } else { + cb(err, module); + } + }); + }; + + compilationHooks.seal.tap('ParallelModulePlugin', () => { + killWorkers(); + }); + }; + + const doChild = () => { + const _create = params.normalModuleFactory.create; + params.normalModuleFactory.create = (data, cb) => {}; + + process.send('ready:' + compiler.__hardSource_configHash); + + process.on('message', _job => { + const job = JSON.parse(_job); + const module = thaw('Module', null, job.data, { + compilation, + normalModuleFactory: params.normalModuleFactory, + contextModuleFactory: params.contextModuleFactory, + }); + + module.build( + compilation.options, + compilation, + compilation.resolverFactory.get('normal', module.resolveOptions), + compilation.inputFileSystem, + error => { + process.send( + JSON.stringify({ + id: job.id, + error: error, + module: + module && + freeze('Module', null, module, { + id: module.identifier(), + compilation, + }), + }), + ); + }, + ); + }); + }; + + if (!process.send) { + doMaster(); + } else { + doChild(); + } + }, + ); + } +} + +module.exports = ParallelModulePlugin; diff --git a/lib/SupportExtractTextPlugin.js b/lib/SupportExtractTextPlugin.js index 8d8c086d..18ad512f 100644 --- a/lib/SupportExtractTextPlugin.js +++ b/lib/SupportExtractTextPlugin.js @@ -20,7 +20,7 @@ class SupportExtractTextPlugin { // that assets get built. if ( module[extractTextNS] || - (!module.buildMeta && module.meta && module.meta[extractTextNS]) + (!module.factoryMeta && module.meta && module.meta[extractTextNS]) ) { return null; } diff --git a/lib/TransformNormalModulePlugin.js b/lib/TransformNormalModulePlugin.js index 1fe3d392..5b2a53dc 100644 --- a/lib/TransformNormalModulePlugin.js +++ b/lib/TransformNormalModulePlugin.js @@ -50,165 +50,190 @@ const serialResolvedMap = serial.map( const serialResourceHashMap = serial.map(serial.request, serial.identity); -const serialNormalModule4 = serial.serial('NormalModule', { - constructor: serial.constructed(NormalModule, { - data: serial.pipe( - { freeze: (arg, module) => module, thaw: arg => arg }, - serial.created({ - type: serial.identity, - request: serial.request, - userRequest: serial.request, - rawRequest: serial.request, - loaders: serial.loaders, - resource: serial.path, - parser: serial.parser, - generator: serial.generator, - resolveOptions: serial.identity, - }), - ), - }), +const serialNormalConstructor4 = serial.constructed(NormalModule, { + data: serial.pipe( + { freeze: (arg, module) => module, thaw: arg => arg }, + serial.created({ + type: serial.identity, + request: serial.request, + userRequest: serial.request, + rawRequest: serial.request, + loaders: serial.loaders, + resource: serial.path, + parser: serial.parser, + generator: serial.generator, + resolveOptions: serial.identity, + }), + ), +}); - setModuleExtra: { - freeze() {}, - thaw(arg, frozen, extra, methods) { - extra.module = arg; - return arg; - }, +const serialNormalModuleExtra4 = { + freeze() {}, + thaw(arg, frozen, extra, methods) { + extra.module = arg; + return arg; }, +}; - identifier: { - freeze(arg, module, extra, methods) { - return serial.request.freeze(module.identifier(), null, extra, methods); - }, - thaw(arg) { - return arg; - }, +const serialNormalIdentifier4 = { + freeze(arg, module, extra, methods) { + return serial.request.freeze(module.identifier(), null, extra, methods); + }, + thaw(arg) { + return arg; }, +}; - assigned: serial.assigned({ - factoryMeta: serial.identity, - issuer: serial.pipe( - { - freeze(arg, { issuer }) { - return issuer && typeof issuer === 'object' - ? issuer.identifier() - : issuer; - }, - thaw(arg, frozen, extra) { - return arg; - }, +const serialNormalAssigned4 = serial.assigned({ + factoryMeta: serial.identity, + issuer: serial.pipe( + { + freeze(arg, { issuer }) { + return issuer && typeof issuer === 'object' + ? issuer.identifier() + : issuer; }, - serial.request, - { - freeze(arg) { - return arg; - }, - thaw(arg, frozen, { compilation }) { - if (compilation.modules) { - for (const module of compilation.modules) { - if ( - module && - typeof module.identifier === 'function' && - module.identifier() === arg - ) { - return module; - } + thaw(arg, frozen, extra) { + return arg; + }, + }, + serial.request, + { + freeze(arg) { + return arg; + }, + thaw(arg, frozen, { compilation }) { + if (compilation.modules) { + for (const module of compilation.modules) { + if ( + module && + typeof module.identifier === 'function' && + module.identifier() === arg + ) { + return module; } - for (const cacheId in compilation.cache) { - const module = compilation.cache[cacheId]; - if ( - module && - typeof module.identifier === 'function' && - module.identifier() === arg - ) { - return module; - } + } + for (const cacheId in compilation.cache) { + const module = compilation.cache[cacheId]; + if ( + module && + typeof module.identifier === 'function' && + module.identifier() === arg + ) { + return module; } } - return arg; - }, + } + return arg; }, - ), - useSourceMap: serial.identity, - lineToLine: serial.identity, - }), - - setOriginExtra: { - freeze() {}, - thaw(arg, frozen, extra) { - if (typeof arg.issuer === 'object') { - extra.origin = arg.issuer; - } - return arg; }, - }, - - build: serial.assigned({ - built: serial.identity, - buildTimestamp: serial.identity, - buildMeta: serial.identity, - buildInfo: serial.created({ - assets: serial.moduleAssets, - cacheable: serial.identity, - contextDependencies: serial.pathSet, - exportsArgument: serial.identity, - fileDependencies: serial.pathSet, - harmonyModule: serial.identity, - jsonData: serial.identity, - strict: serial.identity, - }), - warnings: serial.moduleWarning, - errors: serial.moduleError, - _source: serial.source, - _buildHash: serial.identity, - hash: serial.identity, - _lastSuccessfulBuildMeta: serial.identity, - - __hardSource_resolved: serialResolvedMap, - __hardSource_oldHashes: serial.pipe( - { - freeze(arg, module, extra) { - const obj = {}; - const cachedMd5s = extra.compilation.__hardSourceCachedMd5s; + ), + useSourceMap: serial.identity, + lineToLine: serial.identity, +}); - for (const file of module.buildInfo.fileDependencies) { - obj[file] = cachedMd5s[file]; - } - for (const dir of module.buildInfo.contextDependencies) { - obj[dir] = cachedMd5s[dir]; - } +const serialNormalOriginExtra4 = { + freeze() {}, + thaw(arg, frozen, extra) { + if (typeof arg.issuer === 'object') { + extra.origin = arg.issuer; + } + return arg; + }, +}; - return obj; - }, - thaw: serial.identity.thaw, - }, - serialResourceHashMap, - ), +const serialNormalBuild4 = serial.assigned({ + built: serial.identity, + buildTimestamp: serial.identity, + buildMeta: serial.identity, + buildInfo: serial.created({ + assets: serial.moduleAssets, + cacheable: serial.identity, + contextDependencies: serial.pathSet, + exportsArgument: serial.identity, + fileDependencies: serial.pathSet, + harmonyModule: serial.identity, + jsonData: serial.identity, + strict: serial.identity, }), + warnings: serial.moduleWarning, + errors: serial.moduleError, + _source: serial.source, + _buildHash: serial.identity, + hash: serial.identity, + _lastSuccessfulBuildMeta: serial.identity, + + __hardSource_resolved: serialResolvedMap, + __hardSource_oldHashes: serial.pipe( + { + freeze(arg, module, extra) { + const obj = {}; + const cachedMd5s = extra.compilation.__hardSourceFileMd5s; + + for (const file of module.buildInfo.fileDependencies) { + obj[file] = cachedMd5s[file]; + } + for (const dir of module.buildInfo.contextDependencies) { + obj[dir] = cachedMd5s[dir]; + } - dependencyBlock: serial.dependencyBlock, - - setError: { - freeze() {}, - thaw(arg, module, extra) { - arg.error = arg.errors[0] || null; - return arg; + return obj; + }, + thaw: serial.identity.thaw, }, + serialResourceHashMap, + ), +}); + +const serialNormalError4 = { + freeze() {}, + thaw(arg, module, extra) { + arg.error = arg.errors[0] || null; + return arg; }, +}; - setSourceExtra: { - freeze() {}, - thaw(arg, module, extra) { - extra.source = arg._source; - return arg; - }, +const serialNormalSourceExtra4 = { + freeze() {}, + thaw(arg, module, extra) { + extra.source = arg._source; + return arg; }, +}; - source: serial.assigned({ - _cachedSource: serial.source, - _cachedSourceHash: serial.identity, - renderedHash: serial.identity, - }), +const serialNormalSource4 = serial.assigned({ + _cachedSource: serial.source, + _cachedSourceHash: serial.identity, + renderedHash: serial.identity, +}); + +const serialNormalModule4PreBuild = serial.serial('NormalModule', { + constructor: serialNormalConstructor4, + setModuleExtra: serialNormalModuleExtra4, + identifier: serialNormalIdentifier4, + assigned: serialNormalAssigned4, + setOriginExtra: serialNormalOriginExtra4, +}); + +const serialNormalModule4PostBuild = serial.serial('NormalModule', { + build: serialNormalBuild4, + dependencyBlock: serial.dependencyBlock, + setError: serialNormalError4, + setSourceExtra: serialNormalSourceExtra4, + source: serialNormalSource4, +}); + +const serialNormalModule4 = serial.serial('NormalModule', { + constructor: serialNormalConstructor4, + setModuleExtra: serialNormalModuleExtra4, + identifier: serialNormalIdentifier4, + assigned: serialNormalAssigned4, + setOriginExtra: serialNormalOriginExtra4, + build: serialNormalBuild4, + dependencyBlock: serial.dependencyBlock, + setError: serialNormalError4, + setSourceExtra: serialNormalSourceExtra4, + source: serialNormalSource4, }); const needRebuild4 = function() { @@ -275,23 +300,9 @@ const serialNormalModule3 = serial.serial('NormalModule', { parser: serial.parser, }), - setModuleExtra: { - freeze() {}, - thaw(arg, frozen, extra, methods) { - extra.module = arg; - return arg; - }, - }, - + setModuleExtra: serialNormalModuleExtra4, // Used internally by HardSource - identifier: { - freeze(arg, module, extra, methods) { - return serial.request.freeze(module.identifier(), null, extra, methods); - }, - thaw(arg) { - return arg; - }, - }, + identifier: serialNormalIdentifier4, assigned: serial.assigned({ issuer: serial.pipe( @@ -557,13 +568,18 @@ class TransformNormalModulePlugin { 'TransformNormalModulePlugin', (frozen, module, extra) => { // Set hash if it was not set. - if (schema === 4 && module instanceof NormalModule && !module.hash) { + if ( + schema === 4 && + module instanceof NormalModule && + module.buildTimestamp && + !module.hash + ) { const outputOptions = extra.compilation.outputOptions; const hashFunction = outputOptions.hashFunction; const hashDigest = outputOptions.hashDigest; const hashDigestLength = outputOptions.hashDigestLength; - if (module._initBuildHash) { + if (module.buildInfo && module._initBuildHash) { module._initBuildHash(extra.compilation); } @@ -580,7 +596,7 @@ class TransformNormalModulePlugin { if ( module.request && - cacheable(module) && + (cacheable(module) || !module.built) && module instanceof NormalModule && (!frozen || (schema >= 4 && module.hash !== frozen.build.hash) || @@ -595,7 +611,11 @@ class TransformNormalModulePlugin { module.cacheItem.invalidReason = null; } - const f = serialNormalModule.freeze( + let serialModule = serialNormalModule; + if (!module.built) { + serialModule = serialNormalModule4PreBuild; + } + const f = serialModule.freeze( null, module, { @@ -625,16 +645,34 @@ class TransformNormalModulePlugin { 'TransformNormalModulePlugin thaw', (module, frozen, { compilation, normalModuleFactory }) => { if (frozen.type === 'NormalModule') { - const m = serialNormalModule.thaw( - null, - frozen, - { - state: { imports: {} }, - compilation: compilation, - normalModuleFactory: normalModuleFactory, - }, - _methods, - ); + let m; + if (module === null) { + let serialModule = serialNormalModule; + if (!frozen.build || !frozen.build.built) { + serialModule = serialNormalModule4PreBuild; + } + m = serialModule.thaw( + null, + frozen, + { + state: { imports: {} }, + compilation: compilation, + normalModuleFactory: normalModuleFactory, + }, + _methods, + ); + } else { + m = serialNormalModule4PostBuild.thaw( + module, + frozen, + { + state: { imports: {} }, + compilation: compilation, + normalModuleFactory: normalModuleFactory, + }, + _methods, + ); + } m.cacheItem = frozen; m.__hardSourceFileMd5s = compilation.__hardSourceFileMd5s; @@ -644,12 +682,13 @@ class TransformNormalModulePlugin { // Unbuild if there is no cache. The module will be rebuilt. Not // unbuilding will lead to double dependencies. - if (schema === 4 && !compilation.cache) { + if (m.built && schema === 4 && !compilation.cache) { m.unbuild(); } // Side load into the cache if something for this identifier isn't already // there. else if ( + m.built && compilation.cache && !compilation.cache[`m${m.identifier()}`] ) { diff --git a/lib/util/log-messages.js b/lib/util/log-messages.js index 8341e7d1..62641e4f 100644 --- a/lib/util/log-messages.js +++ b/lib/util/log-messages.js @@ -196,3 +196,53 @@ exports.childCompilerUnnamedCache = compilation => { ].join('\n'), ); }; + +const logParallel = compiler => + new LoggerFactory(compiler).create().from('parallel'); + +exports.parallelStartWorkers = (compiler, options) => { + const loggerParallel = logParallel(compiler); + loggerParallel.log( + { + id: 'parallel--start-workers', + numWorkers: options.numWorkers, + }, + [`Start ${options.numWorkers} module workers.`].join('\n'), + ); +}; + +exports.parallelConfigMismatch = (compiler, options) => { + const loggerParallel = logParallel(compiler); + loggerParallel.error( + { + id: 'parallel--config-mismatch', + ourHash: options.ourHash, + theirHash: options.theirHash, + }, + [ + `Child process's configuration does not match parent `, + `configuration. Unable to parallelize webpack.`, + ].join('\n'), + ); +}; + +exports.parallelErrorSendingJob = (compiler, error) => { + const loggerParallel = logParallel(compiler); + loggerParallel.error( + { + id: 'parallel--error-sending-job', + error, + }, + `Failed to send parallel module work. ${error.stack}`, + ); +}; + +exports.parallelRequireWebpack4 = compiler => { + const loggerParallel = logParallel(compiler); + loggerParallel.error( + { + id: 'parallel--webpack-4', + }, + `Parallel Module Plugin requires webpack 4.`, + ); +}; diff --git a/lib/util/plugin-compat.js b/lib/util/plugin-compat.js index 5c9a3904..01b1b148 100644 --- a/lib/util/plugin-compat.js +++ b/lib/util/plugin-compat.js @@ -18,6 +18,7 @@ const knownPluginRegistrations = { needAdditionalPass: ['sync', []], succeedModule: ['sync', ['module']], buildModule: ['sync', ['module']], + seal: ['sync', []], }, Compiler: { afterCompile: ['asyncSerial', ['compilation']], diff --git a/lib/util/serial.js b/lib/util/serial.js index 1931e12d..1767d9d5 100644 --- a/lib/util/serial.js +++ b/lib/util/serial.js @@ -211,7 +211,7 @@ const serial = (exports.serial = (name, stages) => ({ return out; }, thaw(arg, frozen, extra, methods) { - let out; + let out = arg; for (const key in stages) { out = stages[key].thaw(out, frozen[key], extra, methods); } diff --git a/tests/fixtures/hard-source-parallel-plugin-config-mismatch/fib.js b/tests/fixtures/hard-source-parallel-plugin-config-mismatch/fib.js new file mode 100644 index 00000000..a0a9ae75 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-config-mismatch/fib.js @@ -0,0 +1,3 @@ +module.exports = function(n) { + return n + (n > 0 ? n - 1 : 0); +}; diff --git a/tests/fixtures/hard-source-parallel-plugin-config-mismatch/index.js b/tests/fixtures/hard-source-parallel-plugin-config-mismatch/index.js new file mode 100644 index 00000000..8acf0395 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-config-mismatch/index.js @@ -0,0 +1,3 @@ +var fib = require('./fib'); + +console.log(fib(3)); diff --git a/tests/fixtures/hard-source-parallel-plugin-config-mismatch/webpack.config.js b/tests/fixtures/hard-source-parallel-plugin-config-mismatch/webpack.config.js new file mode 100644 index 00000000..306d8c8f --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-config-mismatch/webpack.config.js @@ -0,0 +1,25 @@ +var HardSourceWebpackPlugin = require('../../..'); + +module.exports = { + // The test process will have mode set by testing utilities. The forked + // parallel processes will not have mode set. With a different configuration + // the processes won't match and building should fallback to the test process. + // mode: 'development', + context: __dirname, + entry: './index.js', + output: { + path: __dirname + '/tmp', + filename: 'main.js', + }, + plugins: [ + new HardSourceWebpackPlugin({ + cacheDirectory: 'cache', + environmentHash: { + root: __dirname + '/../../..', + }, + }), + new (require('../../../lib/ParallelModulePlugin'))({ + minModules: 1, + }), + ], +}; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/a/1.js b/tests/fixtures/hard-source-parallel-plugin-context/a/1.js new file mode 100644 index 00000000..bd816eab --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/a/1.js @@ -0,0 +1 @@ +module.exports = 1; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/a/2.js b/tests/fixtures/hard-source-parallel-plugin-context/a/2.js new file mode 100644 index 00000000..4bbffde1 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/a/2.js @@ -0,0 +1 @@ +module.exports = 2; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/a/3.js b/tests/fixtures/hard-source-parallel-plugin-context/a/3.js new file mode 100644 index 00000000..690aad34 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/a/3.js @@ -0,0 +1 @@ +module.exports = 3; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/a/4.js b/tests/fixtures/hard-source-parallel-plugin-context/a/4.js new file mode 100644 index 00000000..a9bbdd80 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/a/4.js @@ -0,0 +1 @@ +module.exports = 4; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/a/5.js b/tests/fixtures/hard-source-parallel-plugin-context/a/5.js new file mode 100644 index 00000000..f4e8d9d2 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/a/5.js @@ -0,0 +1 @@ +module.exports = 5; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/a/index.js b/tests/fixtures/hard-source-parallel-plugin-context/a/index.js new file mode 100644 index 00000000..22db7dcc --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/a/index.js @@ -0,0 +1,8 @@ +var context = require.context('.', false, /\d/); + +module.exports = + context('./1') + + context('./2') + + context('./3') + + context('./4') + + context('./5'); diff --git a/tests/fixtures/hard-source-parallel-plugin-context/b/10.js b/tests/fixtures/hard-source-parallel-plugin-context/b/10.js new file mode 100644 index 00000000..4387befd --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/b/10.js @@ -0,0 +1 @@ +module.exports = 10; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/b/6.js b/tests/fixtures/hard-source-parallel-plugin-context/b/6.js new file mode 100644 index 00000000..3293ce95 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/b/6.js @@ -0,0 +1 @@ +module.exports = 6; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/b/7.js b/tests/fixtures/hard-source-parallel-plugin-context/b/7.js new file mode 100644 index 00000000..e7377b25 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/b/7.js @@ -0,0 +1 @@ +module.exports = 7; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/b/8.js b/tests/fixtures/hard-source-parallel-plugin-context/b/8.js new file mode 100644 index 00000000..43a63b67 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/b/8.js @@ -0,0 +1 @@ +module.exports = 8; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/b/9.js b/tests/fixtures/hard-source-parallel-plugin-context/b/9.js new file mode 100644 index 00000000..d1934d00 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/b/9.js @@ -0,0 +1 @@ +module.exports = 9; diff --git a/tests/fixtures/hard-source-parallel-plugin-context/b/index.js b/tests/fixtures/hard-source-parallel-plugin-context/b/index.js new file mode 100644 index 00000000..28c3d129 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/b/index.js @@ -0,0 +1,6 @@ +module.exports = + require('./6') + + require('./7') + + require('./8') + + require('./9') + + require('./10'); diff --git a/tests/fixtures/hard-source-parallel-plugin-context/index.js b/tests/fixtures/hard-source-parallel-plugin-context/index.js new file mode 100644 index 00000000..b003d264 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/index.js @@ -0,0 +1 @@ +console.log(require('./a') + require('./b')); diff --git a/tests/fixtures/hard-source-parallel-plugin-context/webpack.config.js b/tests/fixtures/hard-source-parallel-plugin-context/webpack.config.js new file mode 100644 index 00000000..878c12de --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-context/webpack.config.js @@ -0,0 +1,19 @@ +var HardSourceWebpackPlugin = require('../../..'); + +module.exports = { + mode: 'development', + context: __dirname, + entry: './index.js', + output: { + path: __dirname + '/tmp', + filename: 'main.js', + }, + plugins: [ + new HardSourceWebpackPlugin({ + cacheDirectory: 'cache', + }), + new (require('../../../lib/ParallelModulePlugin'))({ + minModules: 2, + }), + ], +}; diff --git a/tests/fixtures/hard-source-parallel-plugin-defaults/fib.js b/tests/fixtures/hard-source-parallel-plugin-defaults/fib.js new file mode 100644 index 00000000..a0a9ae75 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-defaults/fib.js @@ -0,0 +1,3 @@ +module.exports = function(n) { + return n + (n > 0 ? n - 1 : 0); +}; diff --git a/tests/fixtures/hard-source-parallel-plugin-defaults/index.js b/tests/fixtures/hard-source-parallel-plugin-defaults/index.js new file mode 100644 index 00000000..8acf0395 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-defaults/index.js @@ -0,0 +1,3 @@ +var fib = require('./fib'); + +console.log(fib(3)); diff --git a/tests/fixtures/hard-source-parallel-plugin-defaults/webpack.config.js b/tests/fixtures/hard-source-parallel-plugin-defaults/webpack.config.js new file mode 100644 index 00000000..7dd07172 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin-defaults/webpack.config.js @@ -0,0 +1,20 @@ +var HardSourceWebpackPlugin = require('../../..'); + +module.exports = { + mode: 'development', + context: __dirname, + entry: './index.js', + output: { + path: __dirname + '/tmp', + filename: 'main.js', + }, + plugins: [ + new HardSourceWebpackPlugin({ + cacheDirectory: 'cache', + environmentHash: { + root: __dirname + '/../../..', + }, + }), + new (require('../../../lib/ParallelModulePlugin'))(), + ], +}; diff --git a/tests/fixtures/hard-source-parallel-plugin/fib.js b/tests/fixtures/hard-source-parallel-plugin/fib.js new file mode 100644 index 00000000..a0a9ae75 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin/fib.js @@ -0,0 +1,3 @@ +module.exports = function(n) { + return n + (n > 0 ? n - 1 : 0); +}; diff --git a/tests/fixtures/hard-source-parallel-plugin/index.js b/tests/fixtures/hard-source-parallel-plugin/index.js new file mode 100644 index 00000000..8acf0395 --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin/index.js @@ -0,0 +1,3 @@ +var fib = require('./fib'); + +console.log(fib(3)); diff --git a/tests/fixtures/hard-source-parallel-plugin/webpack.config.js b/tests/fixtures/hard-source-parallel-plugin/webpack.config.js new file mode 100644 index 00000000..aabea28c --- /dev/null +++ b/tests/fixtures/hard-source-parallel-plugin/webpack.config.js @@ -0,0 +1,26 @@ +var HardSourceWebpackPlugin = require('../../..'); + +module.exports = { + mode: 'development', + context: __dirname, + entry: './index.js', + output: { + path: __dirname + '/tmp', + filename: 'main.js', + }, + plugins: [ + new HardSourceWebpackPlugin({ + cacheDirectory: 'cache', + environmentHash: { + root: __dirname + '/../../..', + }, + }), + new HardSourceWebpackPlugin.ParallelModulePlugin({ + fork: (fork, compiler, webpackBin) => fork(webpackBin(), ['--config', __filename], { + silent: true, + }), + numWorkers: 2, + minModules: 1, + }), + ], +}; diff --git a/tests/hard-source.js b/tests/hard-source.js index 5e7dd21c..4636ca14 100644 --- a/tests/hard-source.js +++ b/tests/hard-source.js @@ -2,6 +2,7 @@ var fs = require('fs'); var expect = require('chai').expect; +var describeWP = require('./util').describeWP; var itCompiles = require('./util').itCompiles; var itCompilesChange = require('./util').itCompilesChange; var itCompilesHardModules = require('./util').itCompilesHardModules; @@ -288,3 +289,12 @@ describe('hard-source features', function() { }); }); + +describeWP(4)('hard-source webpack 4 features', function() { + + itCompilesTwice('hard-source-parallel-plugin'); + itCompilesTwice('hard-source-parallel-plugin-config-mismatch'); + itCompilesTwice('hard-source-parallel-plugin-context'); + itCompilesTwice('hard-source-parallel-plugin-defaults'); + +}); diff --git a/tests/util/index.js b/tests/util/index.js index 9b82687b..2361704b 100644 --- a/tests/util/index.js +++ b/tests/util/index.js @@ -230,7 +230,7 @@ exports.itCompilesTwice = function(fixturePath, compileOptions) { exportSuffix = ' [exportStats]'; } it('builds identical ' + fixturePath + ' fixture' + exportSuffix, function() { - this.timeout(10000); + this.timeout(30000); return exports.clean(fixturePath) .then(function() { return exports.compileTwiceEqual(fixturePath, compileOptions); @@ -335,7 +335,7 @@ exports.itCompiles = function(name, fixturePath, fns, expectHandle) { // }); it(name, function() { - this.timeout(20000); + this.timeout(30000); this.slow(4000); var runs = []; var setups = []; @@ -381,7 +381,7 @@ exports.itCompilesWithCache = function(name, fixturePath, fnA, fnB, expectHandle }); it(name, function() { - this.timeout(20000); + this.timeout(30000); this.slow(4000); var cache1, cache2; return Promise.resolve()