├── .eslintignore ├── .gitignore ├── test ├── configs │ ├── files │ │ ├── runtime-error.js │ │ ├── simple.js │ │ ├── syntax-error.js │ │ └── exports-render.js │ ├── client-basic.js │ ├── client-syntax-error.js │ ├── server-basic.js │ ├── server-runtime-error.js │ └── server-with-hashes.js ├── util │ ├── touchFile.js │ ├── createCompilation.js │ ├── normalizeHtmlError.js │ ├── createWritter.js │ └── createCompiler.js ├── __snapshots__ │ ├── reporter.spec.js.snap │ ├── check-hashes.spec.js.snap │ └── middleware.spec.js.snap ├── notifier.spec.js ├── reporter.spec.js ├── instantiation.spec.js ├── check-hashes.spec.js └── middleware.spec.js ├── .eslintrc.json ├── lib ├── util │ ├── memoryFs.js │ ├── compilationResolver.js │ └── checkHashes.js ├── mainMiddleware.js ├── renderErrorMiddleware.js └── devMiddleware.js ├── .editorconfig ├── .travis.yml ├── LICENSE ├── package.json ├── index.js ├── CHANGELOG.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | test/tmp/ 3 | test/configs/files/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.* 3 | coverage/ 4 | test/tmp/ 5 | -------------------------------------------------------------------------------- /test/configs/files/runtime-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | foo(); 4 | -------------------------------------------------------------------------------- /test/configs/files/simple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('Hello!'); 4 | -------------------------------------------------------------------------------- /test/configs/files/syntax-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | console.log('Hello!' 4 | -------------------------------------------------------------------------------- /test/configs/files/exports-render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.render = () => {}; 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint-config-moxy/es9", 5 | "eslint-config-moxy/addons/node", 6 | "eslint-config-moxy/addons/jest" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /lib/util/memoryFs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MemoryFileSystem = require('memory-fs'); 4 | 5 | function memoryFs() { 6 | return new MemoryFileSystem(); 7 | } 8 | 9 | module.exports = memoryFs; 10 | -------------------------------------------------------------------------------- /test/util/touchFile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | function touchFile(path) { 6 | fs.writeFileSync(path, fs.readFileSync(path)); 7 | } 8 | 9 | module.exports = touchFile; 10 | -------------------------------------------------------------------------------- /test/configs/client-basic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: path.resolve(`${__dirname}/files/simple.js`), 7 | output: { 8 | path: path.resolve(`${__dirname}/../tmp`), 9 | filename: 'client.js', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/configs/client-syntax-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: path.resolve(`${__dirname}/files/syntax-error.js`), 7 | output: { 8 | path: path.resolve(`${__dirname}/../tmp`), 9 | filename: 'server.js', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [package.json] 13 | indent_size = 2 14 | 15 | [{*.md,*.snap}] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | script: 6 | - "npm test" 7 | - "npm install webpack@^3 --no-save && npm test -- --no-coverage" 8 | - "npm install webpack@^2 --no-save && npm test -- --no-coverage" 9 | # Report coverage 10 | after_success: 11 | - "npm i codecov" 12 | - "node_modules/.bin/codecov" 13 | -------------------------------------------------------------------------------- /test/configs/server-basic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: path.resolve(`${__dirname}/files/exports-render.js`), 7 | target: 'node', 8 | output: { 9 | path: path.resolve(`${__dirname}/../tmp`), 10 | filename: 'server.js', 11 | libraryTarget: 'this', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/configs/server-runtime-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: path.resolve(`${__dirname}/files/runtime-error.js`), 7 | target: 'node', 8 | output: { 9 | path: path.resolve(`${__dirname}/../tmp`), 10 | filename: 'server.js', 11 | libraryTarget: 'this', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/configs/server-with-hashes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: path.resolve(`${__dirname}/files/exports-render.js`), 7 | target: 'node', 8 | output: { 9 | path: path.resolve(`${__dirname}/../tmp`), 10 | filename: 'output.a1aaaaaaaaaaaaaaaaaaaaaaa.js', // Simulate hashes, 11 | libraryTarget: 'this', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /test/__snapshots__/reporter.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should report stats only once by default 1`] = ` 4 | "● Compiling... 5 | ✔ Compilation succeeded (xxx) 6 | 7 | CLIENT 8 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9 | Asset Size Chunks Chunk Names 10 | client.js xxx 11 | 12 | SERVER 13 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14 | Asset Size Chunks Chunk Names 15 | server.js xxx 16 | 17 | ● Compiling... 18 | ✔ Compilation succeeded (xxx) 19 | " 20 | `; 21 | -------------------------------------------------------------------------------- /lib/mainMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const compilationResolver = require('./util/compilationResolver'); 4 | 5 | function mainMiddleware(compiler, options) { 6 | const resolveCompilation = compilationResolver(compiler, options); 7 | 8 | return (req, res, next) => { 9 | // Wait for compilation to be ready 10 | resolveCompilation() 11 | .then((compilation) => { 12 | res.locals.isomorphic = compilation; 13 | }) 14 | // There's no need to use `setImmediate` when calling `next` because express already 15 | // defers the call for us 16 | .then(next, next); 17 | }; 18 | } 19 | 20 | module.exports = mainMiddleware; 21 | -------------------------------------------------------------------------------- /test/util/createCompilation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const merge = require('lodash/merge'); 4 | 5 | module.exports = (overrides) => merge({ 6 | clientStats: { 7 | toString() { 8 | return [ 9 | 'Asset Size', 10 | 'foo.js 10Kb', 11 | ].join('\n'); 12 | }, 13 | 14 | toJson() { 15 | return { 16 | assets: [ 17 | { name: 'foo.js' }, 18 | ], 19 | }; 20 | }, 21 | 22 | startTime: 0, 23 | endTime: 0, 24 | }, 25 | serverStats: { 26 | toString() { 27 | return [ 28 | 'Asset Size', 29 | 'bar.js 10Kb', 30 | ].join('\n'); 31 | }, 32 | 33 | toJson() { 34 | return { 35 | assets: [ 36 | { name: 'bar.js' }, 37 | ], 38 | }; 39 | }, 40 | 41 | startTime: 0, 42 | endTime: 0, 43 | }, 44 | duration: 0, 45 | }, overrides); 46 | -------------------------------------------------------------------------------- /test/util/normalizeHtmlError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const escapeRegExp = require('lodash.escaperegexp'); 5 | 6 | function normalizeHtmlError(html) { 7 | return html 8 | // Remove absolute directory references 9 | .replace(new RegExp(escapeRegExp(process.cwd() + path.sep), 'g'), '') 10 | // Remove stack traces done by pretty-error 11 | // 12 | .replace(/[\s\S]+(The error above was thrown)/, '[stack]\n\n$1') 13 | // Remove config file from "Module parse failed: test/config/files/xxx.js Unexpected token (4:0)" 14 | // because some webpack versions include it, some not 15 | .replace(/\stest\/configs\/files\/[a-z0-9-]+\.js\s+/, ' ') 16 | // Normalize tmp directory 17 | .replace(/\btest[/\\]tmp\/[^/\\]+\//g, 'test/tmp/') 18 | // In some environments, `>` is displayed instead of `|` 19 | .replace(/> <\/span>/, '| ') 20 | // In some Node versions, the line & column is displayed but not in others 21 | .replace(/\s+\d+:\d+\b/, ''); 22 | } 23 | 24 | module.exports = normalizeHtmlError; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Made With MOXY Lda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/util/createWritter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const stripAnsi = require('strip-ansi'); 5 | const escapeRegExp = require('lodash.escaperegexp'); 6 | 7 | function normalizeReporterOutput(str) { 8 | return stripAnsi(str) 9 | // Replace (100ms) with (xxxms) 10 | .replace(/\(\d+ms\)/g, '(xxx)') 11 | // Remove "Asset Size Chunks..." spacing between them 12 | .replace(/Asset\s+Size\s+.+/g, (str) => str.replace(/\s+/g, ' ')) 13 | // Remove asset lines with something standard 14 | .replace(/(\s+[a-z0-9.]+\.[a-z]+\s)\s*(?:\d+\.\d+)\s(?:bytes|ki?b|mi?b|gi?b)\s.+?\n/gi, '$1 xxx\n') 15 | // Remove stack traces done by pretty-error 16 | .replace(new RegExp(`${escapeRegExp(' ')}.+`, 'g'), ' [stack]') 17 | // Coalesce stack to only one entry 18 | .replace(/(\s{4}\[stack\])([\s\S]+\[stack\])*/, '$1') 19 | // Remove absolute directory references 20 | .replace(new RegExp(escapeRegExp(process.cwd() + path.sep), 'g'), ''); 21 | } 22 | 23 | function createWritter() { 24 | let output = ''; 25 | 26 | function write(str) { 27 | output += str; 28 | } 29 | 30 | return Object.assign(write, { 31 | getOutput() { 32 | return stripAnsi(output); 33 | }, 34 | 35 | getReportOutput() { 36 | return normalizeReporterOutput(output); 37 | }, 38 | 39 | reset() { 40 | output = ''; 41 | }, 42 | }); 43 | } 44 | 45 | module.exports = createWritter; 46 | module.exports.normalizeReporterOutput = normalizeReporterOutput; 47 | -------------------------------------------------------------------------------- /test/notifier.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const saneNotifier = require('webpack-sane-compiler-notifier'); 4 | const webpackIsomorphicDevMiddleware = require('../'); 5 | const createCompiler = require('./util/createCompiler'); 6 | const configClientBasic = require('./configs/client-basic'); 7 | const configServerBasic = require('./configs/server-basic'); 8 | 9 | jest.mock('webpack-sane-compiler-notifier', () => jest.fn((compiler) => compiler)); 10 | 11 | beforeEach(() => jest.clearAllMocks()); 12 | 13 | it('should not notify by default', () => { 14 | const compiler = createCompiler(configClientBasic, configServerBasic); 15 | 16 | webpackIsomorphicDevMiddleware(compiler, { report: false }); 17 | compiler.watch = () => {}; 18 | 19 | expect(saneNotifier).toHaveBeenCalledTimes(0); 20 | }); 21 | 22 | it('should notify if options.notify is NOT false', () => { 23 | const compiler = createCompiler(configClientBasic, configServerBasic); 24 | 25 | compiler.watch = () => {}; 26 | 27 | webpackIsomorphicDevMiddleware(compiler, { report: false, notify: true }); 28 | webpackIsomorphicDevMiddleware(compiler, { report: false, notify: {} }); 29 | 30 | expect(saneNotifier).toHaveBeenCalledTimes(2); 31 | expect(saneNotifier).toHaveBeenCalledWith(compiler, {}); 32 | }); 33 | 34 | it('should pass options.notify to webpack-isomorphic-compiler-notifier', () => { 35 | const compiler = createCompiler(configClientBasic, configServerBasic); 36 | 37 | webpackIsomorphicDevMiddleware(compiler, { 38 | report: false, 39 | notify: { title: 'foo' }, 40 | }); 41 | compiler.watch = () => {}; 42 | 43 | expect(saneNotifier).toHaveBeenCalledTimes(1); 44 | expect(saneNotifier).toHaveBeenCalledWith(compiler, { title: 'foo' }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/reporter.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const request = require('supertest'); 5 | const webpackIsomorphicDevMiddleware = require('../'); 6 | const createCompiler = require('./util/createCompiler'); 7 | const createWritter = require('./util/createWritter'); 8 | const touchFile = require('./util/touchFile'); 9 | const configClientBasic = require('./configs/client-basic'); 10 | const configServerBasic = require('./configs/server-basic'); 11 | 12 | afterEach(() => jest.restoreAllMocks()); 13 | afterEach(() => createCompiler.teardown()); 14 | 15 | it('should report stats only once by default', async () => { 16 | const app = express(); 17 | const compiler = createCompiler(configClientBasic, configServerBasic); 18 | const writter = createWritter(); 19 | 20 | app.use(webpackIsomorphicDevMiddleware(compiler, { 21 | report: { write: writter }, 22 | })); 23 | 24 | await request(app) 25 | .get('/client.js') 26 | .expect(200); 27 | 28 | await new Promise((resolve) => { 29 | compiler.once('begin', resolve); 30 | // Need to add a little delay otherwise webpack won't pick it up.. 31 | // This happens because the file is being written while chokidar is not yet ready (`ready` event not yet emitted) 32 | setTimeout(() => touchFile(configServerBasic.entry), 200); 33 | }); 34 | 35 | await request(app) 36 | .get('/client.js') 37 | .expect(200); 38 | 39 | expect(writter.getReportOutput()).toMatchSnapshot(); 40 | }); 41 | 42 | it('should not report anything if options.report is false', async () => { 43 | const app = express(); 44 | const compiler = createCompiler(configClientBasic, configServerBasic); 45 | 46 | jest.spyOn(process.stderr, 'write').mockImplementation(() => {}); 47 | app.use(webpackIsomorphicDevMiddleware(compiler, { 48 | report: false, 49 | })); 50 | 51 | await request(app) 52 | .get('/client.js') 53 | .expect(200); 54 | 55 | expect(process.stderr.write).toHaveBeenCalledTimes(0); 56 | }); 57 | -------------------------------------------------------------------------------- /test/instantiation.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const request = require('supertest'); 5 | const webpack = require('webpack'); 6 | const webpackIsomorphicDevMiddleware = require('../'); 7 | const createCompiler = require('./util/createCompiler'); 8 | const configClientBasic = require('./configs/client-basic'); 9 | const configServerBasic = require('./configs/server-basic'); 10 | 11 | afterEach(() => createCompiler.teardown()); 12 | 13 | it('should support a webpack multi-compiler', () => { 14 | const app = express(); 15 | const multiCompiler = webpack([ 16 | createCompiler.prepareConfig(configClientBasic), 17 | createCompiler.prepareConfig(configServerBasic), 18 | ]); 19 | const middleware = webpackIsomorphicDevMiddleware(multiCompiler, { 20 | report: false, 21 | }); 22 | 23 | createCompiler.push(middleware.compiler); 24 | app.use(middleware); 25 | 26 | return request(app) 27 | .get('/client.js') 28 | .expect(200) 29 | .expect(/Hello!/); 30 | }); 31 | 32 | it('should support two separate webpack compilers', () => { 33 | const app = express(); 34 | const clientCompiler = webpack(createCompiler.prepareConfig(configClientBasic)); 35 | const serverCompiler = webpack(createCompiler.prepareConfig(configServerBasic)); 36 | const middleware = webpackIsomorphicDevMiddleware(clientCompiler, serverCompiler, { 37 | report: false, 38 | }); 39 | 40 | createCompiler.push(middleware.compiler); 41 | app.use(middleware); 42 | 43 | return request(app) 44 | .get('/client.js') 45 | .expect(200) 46 | .expect(/Hello!/); 47 | }); 48 | 49 | it('should support an isomorphic compiler', () => { 50 | const app = express(); 51 | const compiler = createCompiler(configClientBasic, configServerBasic); 52 | 53 | app.use(webpackIsomorphicDevMiddleware(compiler, { 54 | report: false, 55 | })); 56 | 57 | return request(app) 58 | .get('/client.js') 59 | .expect(200) 60 | .expect(/Hello!/); 61 | }); 62 | 63 | it('should give access to the isomorphic compiler', () => { 64 | const compiler = createCompiler(configClientBasic, configServerBasic); 65 | const middleware = webpackIsomorphicDevMiddleware(compiler); 66 | 67 | compiler.watch = () => {}; 68 | 69 | expect(middleware.compiler).toBe(compiler); 70 | }); 71 | 72 | it('should throw an error on invalid args', () => { 73 | expect(() => webpackIsomorphicDevMiddleware()).toThrow(/invalid arg/i); 74 | }); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-isomorphic-dev-middleware", 3 | "version": "4.1.0", 4 | "description": "The webpack-dev-middleware, but for isomorphic applications.", 5 | "keywords": [ 6 | "webpack", 7 | "isomorphic", 8 | "server-side", 9 | "rendering", 10 | "render", 11 | "middleware", 12 | "express" 13 | ], 14 | "author": "André Cruz ", 15 | "license": "MIT", 16 | "bugs": "https://github.com/moxystudio/webpack-isomorphic-dev-middleware/issues", 17 | "homepage": "https://github.com/moxystudio/webpack-isomorphic-dev-middleware", 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:moxystudio/webpack-isomorphic-dev-middleware" 21 | }, 22 | "main": "index.js", 23 | "files": [ 24 | "lib" 25 | ], 26 | "scripts": { 27 | "lint": "eslint .", 28 | "test": "jest --env node --coverage", 29 | "prerelease": "npm t && npm run lint", 30 | "release": "standard-version", 31 | "precommit": "lint-staged", 32 | "commitmsg": "commitlint -e $GIT_PARAMS", 33 | "postrelease": "git push --follow-tags origin HEAD && npm publish" 34 | }, 35 | "lint-staged": { 36 | "*.js": [ 37 | "eslint --fix", 38 | "git add" 39 | ] 40 | }, 41 | "commitlint": { 42 | "extends": [ 43 | "@commitlint/config-conventional" 44 | ] 45 | }, 46 | "peerDependencies": { 47 | "webpack": ">=2.0.0 <5.0.0", 48 | "express": "^4.0.0" 49 | }, 50 | "dependencies": { 51 | "anser": "^1.3.0", 52 | "chalk": "^2.0.0", 53 | "compose-middleware": "^5.0.0", 54 | "lodash.castarray": "^4.4.0", 55 | "lodash.merge": "^4.6.0", 56 | "lodash.omitby": "^4.6.0", 57 | "memory-fs": "^0.4.1", 58 | "mkdirp": "^0.5.1", 59 | "p-props": "^1.0.0", 60 | "require-from-string": "^2.0.0", 61 | "webpack-dev-middleware": "^3.0.0", 62 | "webpack-isomorphic-compiler": "^3.1.0", 63 | "webpack-isomorphic-compiler-reporter": "^1.3.3", 64 | "webpack-sane-compiler-notifier": "^2.1.0" 65 | }, 66 | "devDependencies": { 67 | "@commitlint/cli": "^7.0.0", 68 | "@commitlint/config-conventional": "^7.0.0", 69 | "eslint": "^5.3.0", 70 | "eslint-config-moxy": "^6.0.1", 71 | "express": "^4.15.2", 72 | "husky": "^0.14.0", 73 | "jest": "^24.5.0", 74 | "lint-staged": "^7.2.0", 75 | "lodash.escaperegexp": "^4.1.2", 76 | "standard-version": "^4.2.0", 77 | "strip-ansi": "^5.0.0", 78 | "supertest": "^3.0.0", 79 | "webpack": "^4.16.2" 80 | }, 81 | "jest": { 82 | "modulePathIgnorePatterns": [ 83 | "test/tmp/", 84 | "test/configs/" 85 | ], 86 | "coveragePathIgnorePatterns": [ 87 | "test/" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/util/compilationResolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const pProps = require('p-props'); 5 | const requireFromString = require('require-from-string'); 6 | 7 | function getServerFile(webpackConfig, stats, options) { 8 | const statsJson = stats.toJson({ 9 | publicPath: false, 10 | outputPath: false, 11 | performance: false, 12 | hash: false, 13 | timings: false, 14 | builtAt: false, 15 | chunks: false, 16 | chunkGroups: false, 17 | modules: false, 18 | children: false, 19 | assets: true, 20 | version: false, 21 | }); 22 | 23 | const serverAsset = options.findServerAssetName(statsJson); 24 | 25 | /* istanbul ignore else */ 26 | if (serverAsset) { 27 | return path.resolve(`${webpackConfig.output.path}/${serverAsset}`); 28 | } 29 | 30 | /* istanbul ignore next */ 31 | throw new Error('Unable to get built server file'); 32 | } 33 | 34 | function loadExports(compiler, options) { 35 | const { webpackConfig, webpackCompiler } = compiler.server; 36 | 37 | // Get the absolute path for the server file (bundle) 38 | const serverFile = getServerFile(webpackConfig, compiler.getCompilation().serverStats, options); 39 | 40 | // Read the file contents 41 | return new Promise((resolve, reject) => { 42 | webpackCompiler.outputFileSystem.readFile(serverFile, (err, buffer) => { 43 | if (err) { 44 | reject(err); 45 | } else { 46 | resolve(buffer.toString()); 47 | } 48 | }); 49 | }) 50 | // Eval as a nodejs module 51 | .then((source) => requireFromString(source, serverFile)) 52 | .catch((err) => { 53 | // Add extra info to the error 54 | err.detail = 'The error above was thrown while trying to load the built server file:\n'; 55 | err.detail += path.relative('', serverFile); 56 | throw err; 57 | }); 58 | } 59 | 60 | function compilationResolver(compiler, options) { 61 | let promise; 62 | 63 | return () => ( 64 | // Wait for the stats 65 | compiler.resolve() 66 | // If the stats are the same, fulfill with a cached compilation 67 | // Otherwise, reload exports 68 | .then((compilation) => { 69 | if (promise && promise.compilation === compilation) { 70 | return promise; 71 | } 72 | 73 | promise = pProps({ 74 | compilation, 75 | exports: loadExports(compiler, options), 76 | }); 77 | promise.compilation = compilation; 78 | 79 | return promise; 80 | }) 81 | ); 82 | } 83 | 84 | module.exports = compilationResolver; 85 | -------------------------------------------------------------------------------- /test/util/createCompiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const pify = require('pify'); 5 | const pFinally = require('p-finally'); 6 | const rimraf = pify(require('rimraf')); 7 | const webpack = require('webpack'); 8 | const webpackIsomorphicCompiler = require('webpack-isomorphic-compiler'); 9 | 10 | const supportsMode = !!webpack.version; 11 | const tmpDir = path.resolve(`${__dirname}/../tmp`); 12 | const compilers = []; 13 | 14 | function createCompiler(clientWebpackConfig, serverWebpackConfig) { 15 | clientWebpackConfig = prepareConfig(clientWebpackConfig); 16 | serverWebpackConfig = prepareConfig(serverWebpackConfig); 17 | 18 | const compiler = webpackIsomorphicCompiler(clientWebpackConfig, serverWebpackConfig); 19 | 20 | compilers.push(compiler); 21 | 22 | return compiler; 23 | } 24 | 25 | function teardown() { 26 | const promises = compilers.map((compiler) => { 27 | // Clear all listeners 28 | compiler 29 | .removeAllListeners() 30 | .on('error', () => {}); 31 | 32 | return pFinally( 33 | // Unwatch 34 | compiler.unwatch() 35 | // Wait for compilation.. just in case.. 36 | .then(() => { 37 | if (!compiler.isCompiling()) { 38 | return; 39 | } 40 | 41 | return new Promise((resolve) => { 42 | compiler 43 | .on('end', () => resolve()) 44 | .on('error', () => resolve()); 45 | }); 46 | }), 47 | // Remove output dirs 48 | () => Promise.all([ 49 | rimraf(compiler.client.webpackConfig.output.path), 50 | rimraf(compiler.server.webpackConfig.output.path), 51 | ]) 52 | ); 53 | }); 54 | 55 | return Promise.all(promises); 56 | } 57 | 58 | function prepareConfig(webpackConfig) { 59 | if (webpackConfig.output.path.indexOf(tmpDir) !== 0) { 60 | throw new Error(`\`webpackConfig.output.path\` must start with ${tmpDir}`); 61 | } 62 | 63 | // Uniquify config 64 | const uid = `${Math.round(Math.random() * 100000000000).toString(36)}-${Date.now().toString(36)}`; 65 | 66 | webpackConfig = { ...webpackConfig }; 67 | webpackConfig.output = { ...webpackConfig.output }; 68 | webpackConfig.output.path = webpackConfig.output.path.replace(tmpDir, path.join(tmpDir, uid)); 69 | 70 | // Ensure mode is set to development on webpack >= 4 71 | if (supportsMode) { 72 | webpackConfig.mode = 'development'; 73 | } 74 | 75 | return webpackConfig; 76 | } 77 | 78 | function push(compiler) { 79 | compilers.push(compiler); 80 | } 81 | 82 | module.exports = createCompiler; 83 | module.exports.teardown = teardown; 84 | module.exports.prepareConfig = prepareConfig; 85 | module.exports.push = push; 86 | -------------------------------------------------------------------------------- /lib/util/checkHashes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chalk = require('chalk'); 4 | 5 | const configProperties = [ 6 | 'output.filename', 7 | 'output.chunkFilename', 8 | 'output.hotUpdateMainFilename', 9 | 'output.hotUpdateChunkFilename', 10 | ]; 11 | 12 | function verifyAssets(compilation, options) { 13 | const write = options.report ? options.report.write : (msg) => process.stderr.write(msg); 14 | const { types, assets } = ['client', 'server'].reduce((detected, type) => { 15 | const statsJson = compilation[`${type}Stats`].toJson({ 16 | assets: true, 17 | chunks: false, 18 | version: false, 19 | children: false, 20 | modules: false, 21 | colors: true, 22 | timings: false, 23 | hash: false, 24 | }); 25 | 26 | // Try to detect hashes the best we can, see http://regexr.com/3frlr 27 | // We can't check webpackConfig.output.filename and other fields because there are loaders & 28 | // plugins that also emit assets; this is the most reliable way 29 | const assetsWithHash = statsJson.assets.filter(({ name }) => 30 | /(^|[^0-9a-z])(?=[a-z]*\d)(?=\d*[a-z])[0-9a-z]{10,}[^0-9a-z]/i.test(name)); 31 | 32 | if (assetsWithHash.length) { 33 | detected.assets.push(...assetsWithHash); 34 | detected.types.push(type); 35 | } 36 | 37 | return detected; 38 | }, { assets: [], types: [] }); 39 | 40 | if (!assets.length) { 41 | return false; 42 | } 43 | 44 | // At this point, assets with hashes were encountered 45 | // Print a good warning, using `options.report.write` if available 46 | 47 | let str; 48 | 49 | str = `${chalk.yellow('WARN')}: Assets with an hash in its name were detected on the `; 50 | str += `${types.map((type) => chalk.bold(type)).join(' and ')}:\n`; 51 | 52 | assets.forEach((asset) => { 53 | str += `- ${asset.name}\n`; 54 | }); 55 | 56 | str += ` 57 | This is known to cause ${chalk.bold('memory leaks')} with ${chalk.bold('webpack-dev-middleware\'s')} in-memory filesystem. 58 | You should avoid using ${chalk.bold('[hash]')} in ${configProperties.join(', ')} as well as similar options in loaders & plugins. 59 | Alternatively, you may set \`memoryFs\` to false altough it will still create many files in the output folder. 60 | If you feel this was a false positive, please ignore this warning. 61 | 62 | `; 63 | 64 | write(str); 65 | 66 | return true; 67 | } 68 | 69 | function checkHashes(compiler, options) { 70 | // Check if files with hashes are being emitted 71 | // This is known to have memory leaks because files stay in memory forever 72 | // See https://github.com/webpack/webpack/issues/2662#issuecomment-252499743 73 | compiler.once('end', (compilation) => { 74 | // If the first verification was ok, do another one on the second compilation (rebuild) 75 | // This is necessary to verify if `hot-update` files are not hashed 76 | if (!verifyAssets(compilation, options)) { 77 | compiler.once('end', (compilation) => verifyAssets(compilation, options)); 78 | } 79 | }); 80 | } 81 | 82 | module.exports = checkHashes; 83 | -------------------------------------------------------------------------------- /test/check-hashes.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const request = require('supertest'); 5 | const webpackIsomorphicDevMiddleware = require('../'); 6 | const createCompiler = require('./util/createCompiler'); 7 | const createCompilation = require('./util/createCompilation'); 8 | const createWritter = require('./util/createWritter'); 9 | const configClientBasic = require('./configs/client-basic'); 10 | const configServerBasic = require('./configs/server-basic'); 11 | const configServerWithHashes = require('./configs/server-with-hashes'); 12 | 13 | afterEach(() => jest.restoreAllMocks()); 14 | afterEach(() => createCompiler.teardown()); 15 | 16 | it('should warn if hashes are being used in webpack config', async () => { 17 | const app = express(); 18 | const compiler = createCompiler(configClientBasic, configServerWithHashes); 19 | const writter = createWritter(); 20 | 21 | app.use(webpackIsomorphicDevMiddleware(compiler, { 22 | report: { write: writter }, 23 | })); 24 | 25 | await request(app) 26 | .get('/client.js') 27 | .expect(200); 28 | 29 | expect(writter.getReportOutput()).toMatchSnapshot(); 30 | }); 31 | 32 | it('should warn if hashes are being used in webpack config (rebuild)', () => { 33 | const app = express(); 34 | const compiler = createCompiler(configClientBasic, configServerBasic); 35 | const writter = createWritter(); 36 | 37 | compiler.watch = () => {}; 38 | 39 | app.use(webpackIsomorphicDevMiddleware(compiler, { 40 | report: { write: writter }, 41 | })); 42 | 43 | compiler.emit('begin'); 44 | compiler.emit('end', createCompilation()); 45 | compiler.emit('begin'); 46 | compiler.emit('end', createCompilation({ 47 | clientStats: { 48 | toJson: () => ({ 49 | assets: [{ name: 'foo.a1aaaaaaaaaaaaaaaaaaaaaaa.js' }], 50 | }), 51 | }, 52 | })); 53 | compiler.emit('begin'); 54 | compiler.emit('end', createCompilation({ 55 | clientStats: { 56 | toJson: () => ({ 57 | assets: [{ name: 'foo.a2aaaaaaaaaaaaaaaaaaaaaaa.js' }], 58 | }), 59 | }, 60 | })); 61 | 62 | expect(writter.getReportOutput()).toMatchSnapshot(); 63 | }); 64 | 65 | it('should not warn about hashes are being used in webpack config if options.memoryFs is false', async () => { 66 | const app = express(); 67 | const compiler = createCompiler(configClientBasic, configServerWithHashes); 68 | const writter = createWritter(); 69 | 70 | app.use(webpackIsomorphicDevMiddleware(compiler, { 71 | memoryFs: false, 72 | report: { write: writter }, 73 | })); 74 | 75 | await request(app) 76 | .get('/client.js') 77 | .expect(200); 78 | 79 | expect(writter.getReportOutput()).toMatchSnapshot(); 80 | }); 81 | 82 | it('should fallback to `process.stderr.write` when printing the warnings', async () => { 83 | const app = express(); 84 | const compiler = createCompiler(configClientBasic, configServerWithHashes); 85 | 86 | jest.spyOn(process.stderr, 'write').mockImplementation(() => {}); 87 | app.use(webpackIsomorphicDevMiddleware(compiler, { 88 | report: false, 89 | })); 90 | 91 | await request(app) 92 | .get('/client.js') 93 | .expect(200); 94 | 95 | expect(process.stderr.write).toHaveBeenCalledTimes(1); 96 | expect(process.stderr.write.mock.calls[0]).toMatchSnapshot(); 97 | }); 98 | -------------------------------------------------------------------------------- /test/__snapshots__/check-hashes.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should fallback to \`process.stderr.write\` when printing the warnings 1`] = ` 4 | Array [ 5 | "WARN: Assets with an hash in its name were detected on the server: 6 | - output.a1aaaaaaaaaaaaaaaaaaaaaaa.js 7 | 8 | This is known to cause memory leaks with webpack-dev-middleware's in-memory filesystem. 9 | You should avoid using [hash] in output.filename, output.chunkFilename, output.hotUpdateMainFilename, output.hotUpdateChunkFilename as well as similar options in loaders & plugins. 10 | Alternatively, you may set \`memoryFs\` to false altough it will still create many files in the output folder. 11 | If you feel this was a false positive, please ignore this warning. 12 | 13 | ", 14 | ] 15 | `; 16 | 17 | exports[`should not warn about hashes are being used in webpack config if options.memoryFs is false 1`] = ` 18 | "● Compiling... 19 | ✔ Compilation succeeded (xxx) 20 | 21 | CLIENT 22 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23 | Asset Size Chunks Chunk Names 24 | client.js xxx 25 | 26 | SERVER 27 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 28 | Asset Size Chunks Chunk Names 29 | output.a1aaaaaaaaaaaaaaaaaaaaaaa.js xxx 30 | 31 | " 32 | `; 33 | 34 | exports[`should warn if hashes are being used in webpack config (rebuild) 1`] = ` 35 | "● Compiling... 36 | ✔ Compilation succeeded (xxx) 37 | 38 | CLIENT 39 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40 | Asset Size foo.js 10Kb 41 | 42 | SERVER 43 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 44 | Asset Size bar.js 10Kb 45 | 46 | ● Compiling... 47 | ✔ Compilation succeeded (xxx) 48 | WARN: Assets with an hash in its name were detected on the client: 49 | - foo.a1aaaaaaaaaaaaaaaaaaaaaaa.js 50 | 51 | This is known to cause memory leaks with webpack-dev-middleware's in-memory filesystem. 52 | You should avoid using [hash] in output.filename, output.chunkFilename, output.hotUpdateMainFilename, output.hotUpdateChunkFilename as well as similar options in loaders & plugins. 53 | Alternatively, you may set \`memoryFs\` to false altough it will still create many files in the output folder. 54 | If you feel this was a false positive, please ignore this warning. 55 | 56 | ● Compiling... 57 | ✔ Compilation succeeded (xxx) 58 | " 59 | `; 60 | 61 | exports[`should warn if hashes are being used in webpack config 1`] = ` 62 | "● Compiling... 63 | ✔ Compilation succeeded (xxx) 64 | 65 | CLIENT 66 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 67 | Asset Size Chunks Chunk Names 68 | client.js xxx 69 | 70 | SERVER 71 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 72 | Asset Size Chunks Chunk Names 73 | output.a1aaaaaaaaaaaaaaaaaaaaaaa.js xxx 74 | 75 | WARN: Assets with an hash in its name were detected on the server: 76 | - output.a1aaaaaaaaaaaaaaaaaaaaaaa.js 77 | 78 | This is known to cause memory leaks with webpack-dev-middleware's in-memory filesystem. 79 | You should avoid using [hash] in output.filename, output.chunkFilename, output.hotUpdateMainFilename, output.hotUpdateChunkFilename as well as similar options in loaders & plugins. 80 | Alternatively, you may set \`memoryFs\` to false altough it will still create many files in the output folder. 81 | If you feel this was a false positive, please ignore this warning. 82 | 83 | " 84 | `; 85 | -------------------------------------------------------------------------------- /lib/renderErrorMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { renderers } = require('webpack-isomorphic-compiler-reporter'); 4 | const anser = require('anser'); 5 | 6 | function createHtmlDocument(message) { 7 | const body = anser.ansiToHtml(anser.escapeForHtml(message), { use_classes: true }); // eslint-disable-line camelcase 8 | 9 | // CSS styles based on https://github.com/glenjamin/webpack-hot-middleware/blob/master/client-overlay.js#L5 with some slight changes 10 | return ` 11 | 12 | 13 | 14 | webpack-isomorphic-dev-middleware error 15 | 50 | 51 | ${body} 52 | `; 53 | } 54 | 55 | function shouldPrintError(err, req) { 56 | // Do not print in tests 57 | return process.env.NODE_ENV !== 'test' && 58 | // Do not print if this is a webpack error since the `webpack-isomorphic-compiler-reporter` will 59 | // be already printing the error 60 | !(err.stats && typeof err.stats.hasErrors === 'function') && 61 | // Do not print if the requested URL is from HMR 62 | req.url !== '/__webpack_hmr'; 63 | } 64 | 65 | function renderErrorMiddleware(compiler, options) { 66 | return function (err, req, res, next) { // eslint-disable-line no-unused-vars 67 | let message = renderers.error(err); 68 | 69 | // Add detail into the message if defined 70 | // This is added to provide more information, e.g.: when failing to load the server file 71 | if (err.detail) { 72 | message += `\n\n${err.detail}`; 73 | } 74 | 75 | // Print error to the console 76 | /* istanbul ignore if */ 77 | if (shouldPrintError(err, req)) { 78 | options.report.write(`${message}\n\n`); 79 | } 80 | 81 | // Render error in the browser 82 | res 83 | .status(500) 84 | .send(createHtmlDocument(message)); 85 | }; 86 | } 87 | 88 | module.exports = renderErrorMiddleware; 89 | -------------------------------------------------------------------------------- /lib/devMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const compose = require('compose-middleware').compose; 4 | const webpackMiddleware = require('webpack-dev-middleware'); 5 | 6 | function createStubbedWebpackCompiler(webpackCompiler) { 7 | // Make `run` and `watch` no-ops 8 | // Additionally, we don't want the dev-middleware to be notified of anything, except for the `done` hook 9 | const doneHandlers = []; 10 | 11 | const stubbedWebpackCompilerHooks = new Proxy({}, { 12 | get(target, property) { 13 | if (property === 'done') { 14 | return { 15 | tap: (name, handler) => doneHandlers.push(handler), 16 | }; 17 | } 18 | 19 | return { 20 | tap: () => {}, 21 | }; 22 | }, 23 | set() { 24 | /* istanbul ignore next */ 25 | return true; 26 | }, 27 | }); 28 | 29 | const stubbedWebpackCompiler = new Proxy(webpackCompiler, { 30 | get(target, property) { 31 | if (property === 'run' || property === 'watch') { 32 | return () => {}; 33 | } 34 | 35 | // The plugin API is for webpack <= v3 36 | /* istanbul ignore if */ 37 | if (property === 'plugin') { 38 | return (name, handler) => { 39 | if (name === 'done') { 40 | doneHandlers.push(handler); 41 | } 42 | }; 43 | } 44 | 45 | // The hooks API is for webpack >= v4 46 | if (property === 'hooks') { 47 | return stubbedWebpackCompilerHooks; 48 | } 49 | 50 | return target[property]; 51 | }, 52 | set() { 53 | // Do not modify any property of the compiler, specially the `outputFileSystem` 54 | return true; 55 | }, 56 | }); 57 | 58 | return { 59 | stubbedWebpackCompiler, 60 | notifyDone: (stats) => doneHandlers.forEach((handler) => handler(stats)), 61 | }; 62 | } 63 | 64 | function devMiddleware(compiler, options) { 65 | const { webpackCompiler, webpackConfig } = compiler.client; 66 | 67 | // We are going to pass a stubbed `webpack-dev-middleware` 68 | // This guarantees that it interoperates well with our own middleware 69 | const { stubbedWebpackCompiler, notifyDone } = createStubbedWebpackCompiler(webpackCompiler); 70 | 71 | // Create the middleware 72 | const devMiddleware = webpackMiddleware(stubbedWebpackCompiler, { 73 | logLevel: 'silent', // We have our own reporter 74 | watchOptions: undefined, // It's not this middleware that calls `.watch()` 75 | publicPath: webpackConfig.output.publicPath, // Why doesn't webpack do this under the hood?! 76 | index: 'some-file-that-will-never-exist', 77 | headers: options.headers, 78 | }); 79 | 80 | // Make sure webpack-dev-middleware is using our own compiler `outputFileSystem` 81 | if (webpackCompiler.outputFileSystem !== devMiddleware.fileSystem) { 82 | for (const key in webpackCompiler.outputFileSystem) { 83 | if (typeof webpackCompiler.outputFileSystem[key] === 'function') { 84 | devMiddleware.fileSystem[key] = webpackCompiler.outputFileSystem[key].bind(webpackCompiler.outputFileSystem); 85 | } 86 | } 87 | } 88 | 89 | // Return final middleware 90 | return compose([ 91 | // Call done callbacks so that the dev middleware get the new stats 92 | (req, res, next) => { 93 | const { clientStats } = res.locals.isomorphic.compilation; 94 | 95 | notifyDone(clientStats); 96 | next(); 97 | }, 98 | devMiddleware, 99 | ]); 100 | } 101 | 102 | module.exports = devMiddleware; 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpackIsomorphicCompiler = require('webpack-isomorphic-compiler'); 4 | const startReporting = require('webpack-isomorphic-compiler-reporter'); 5 | const startNotifying = require('webpack-sane-compiler-notifier'); 6 | const compose = require('compose-middleware').compose; 7 | const merge = require('lodash.merge'); 8 | const omitBy = require('lodash.omitby'); 9 | const castArray = require('lodash.castarray'); 10 | const memoryFs = require('./lib/util/memoryFs'); 11 | const mainMiddleware = require('./lib/mainMiddleware'); 12 | const devMiddleware = require('./lib/devMiddleware'); 13 | const renderErrorMiddleware = require('./lib/renderErrorMiddleware'); 14 | const checkHashes = require('./lib/util/checkHashes'); 15 | 16 | function parseArgs(args) { 17 | const [firstArg = {}, secondArg, thirdArg] = args; 18 | 19 | // Webpack multi compiler? 20 | if (firstArg.compilers && firstArg.run) { 21 | return { 22 | compiler: webpackIsomorphicCompiler(firstArg.compilers[0], firstArg.compilers[1]), 23 | options: parseOptions(secondArg), 24 | }; 25 | } 26 | 27 | // Isomorphic compiler? 28 | if (firstArg.resolve && firstArg.client) { 29 | return { 30 | compiler: firstArg, 31 | options: parseOptions(secondArg), 32 | }; 33 | } 34 | 35 | // Separate webpack compilers 36 | if (firstArg.run && secondArg && secondArg.run) { 37 | return { 38 | compiler: webpackIsomorphicCompiler(firstArg, secondArg), 39 | options: parseOptions(thirdArg), 40 | }; 41 | } 42 | 43 | throw new TypeError('Invalid arguments passed to middleware'); 44 | } 45 | 46 | function parseOptions(options) { 47 | options = merge({ 48 | memoryFs: true, // Enable memory fs 49 | watchDelay: 0, 50 | watchOptions: undefined, // Options to pass to .watch() 51 | report: { stats: 'once' }, // Enable reporting, see https://github.com/moxystudio/webpack-isomorphic-compiler-reporter 52 | notify: false, // Enable OS notifications, see https://github.com/moxystudio/webpack-sane-compiler-notifier 53 | headers: { 'Cache-Control': 'max-age=0, must-revalidate' }, // Headers to set when serving compiled files 54 | findServerAssetName: (stats) => { 55 | const entrypoint = Object.keys(stats.entrypoints)[0]; 56 | 57 | return castArray(stats.assetsByChunkName[entrypoint]) 58 | .find((asset) => /\.js$/.test(asset)); 59 | }, 60 | }, options); 61 | 62 | // Normalize some options 63 | /* istanbul ignore next */ 64 | options.report = options.report === true ? {} : options.report; 65 | options.notify = options.notify === true ? {} : options.notify; 66 | options.headers = omitBy(options.headers, (value) => value == null); 67 | 68 | return options; 69 | } 70 | 71 | function webpackIsomorphicDevMiddleware(...args) { 72 | const { compiler, options } = parseArgs(args); 73 | 74 | // Use an in-memory filesystem 75 | if (options.memoryFs) { 76 | const fs = memoryFs(); 77 | 78 | compiler.client.webpackCompiler.outputFileSystem = fs; 79 | compiler.server.webpackCompiler.outputFileSystem = fs; 80 | } 81 | 82 | // Enable reporting 83 | if (options.report !== false) { 84 | options.report = startReporting(compiler, options.report).options; 85 | } 86 | 87 | // Notify build status through OS notifications 88 | if (options.notify !== false) { 89 | options.notify = startNotifying(compiler, options.notify).options; 90 | } 91 | 92 | // Check if hashes are present in emitted files 93 | options.memoryFs && checkHashes(compiler, options); 94 | 95 | // Create middleware by composing our parts 96 | const middleware = compose([ 97 | mainMiddleware(compiler, options), 98 | devMiddleware(compiler, options), 99 | renderErrorMiddleware(compiler, options), 100 | ]); 101 | 102 | // Expose isomorphic compiler 103 | middleware.compiler = compiler; 104 | 105 | // Start watching 106 | setTimeout(() => compiler.watch(options.watchOptions), options.watchDelay); 107 | 108 | return middleware; 109 | } 110 | 111 | module.exports = webpackIsomorphicDevMiddleware; 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [4.1.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v4.0.3...v4.1.0) (2018-07-26) 7 | 8 | 9 | ### Features 10 | 11 | * add findServerAssetName option ([265f5e4](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/265f5e4)) 12 | 13 | 14 | 15 | 16 | ## [4.0.3](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v4.0.2...v4.0.3) (2018-04-29) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * fix files being served from the cache while they have changed ([f311345](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/f311345)) 22 | 23 | 24 | 25 | 26 | ## [4.0.2](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v4.0.1...v4.0.2) (2018-04-29) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * do not assume that the first asset is a js one ([6824992](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/6824992)) 32 | 33 | 34 | 35 | 36 | ## [4.0.1](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v4.0.0...v4.0.1) (2018-03-10) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * do not print webpack errors to console ([0a68c97](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/0a68c97)) 42 | 43 | 44 | 45 | 46 | # [4.0.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v3.2.2...v4.0.0) (2018-03-06) 47 | 48 | 49 | ### Features 50 | 51 | * support webpack v4 ([#54](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/issues/54)) ([4b68325](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/4b68325)) 52 | 53 | 54 | ### BREAKING CHANGES 55 | 56 | * a peer dependency warning appears when using webpack v2 and v3, so it's safer to release this as a major 57 | 58 | 59 | 60 | 61 | ## [3.2.2](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v3.2.1...v3.2.2) (2018-02-27) 62 | 63 | 64 | 65 | 66 | ## [3.2.1](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v3.2.0...v3.2.1) (2018-02-27) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * check hashes on second recompilation (rebuild) ([#51](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/issues/51)) ([9c32b1a](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/9c32b1a)) 72 | 73 | 74 | 75 | 76 | # [3.2.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v3.1.0...v3.2.0) (2018-02-03) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * **package:** update webpack-isomorphic-compiler to version 3.0.0 ([726552e](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/726552e)) 82 | 83 | 84 | ### Features 85 | 86 | * update webpack-isomorphic-compiler to v3 ([58b63ea](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/58b63ea)) 87 | 88 | 89 | 90 | 91 | # [3.1.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v3.0.1...v3.1.0) (2018-01-15) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * **package:** update compose-middleware to version 4.0.0 ([40db91f](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/40db91f)) 97 | 98 | 99 | ### Features 100 | 101 | * only override the compiler's output `fs` when `memoryFs` is true ([89ae9b8](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/89ae9b8)) 102 | 103 | 104 | 105 | 106 | ## [3.0.1](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v3.0.0...v3.0.1) (2017-12-21) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * fix print of errors to console ([3eb2c7f](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/3eb2c7f)) 112 | 113 | 114 | 115 | 116 | # [3.0.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v2.0.1...v3.0.0) (2017-12-18) 117 | 118 | 119 | ### Chores 120 | 121 | * update webpack-isomorphic-compiler and res.locals signature ([95787fa](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/95787fa)) 122 | 123 | 124 | ### BREAKING CHANGES 125 | 126 | * webpack-isomorphic-compiler now accepts different values for the `report` option. 127 | * res.locals signature has changed 128 | 129 | 130 | 131 | 132 | ## [2.0.1](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v2.0.0...v2.0.1) (2017-12-06) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * fix check of human errors accessing undefined stats in some cases ([553a401](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/553a401)) 138 | 139 | 140 | 141 | 142 | # [2.0.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v1.5.0...v2.0.0) (2017-11-14) 143 | 144 | 145 | ### Features 146 | 147 | * add options.watchDelay ([c33ce18](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/c33ce18)) 148 | 149 | 150 | ### BREAKING CHANGES 151 | 152 | * remove ability to pass false to options.watchDelay 153 | 154 | 155 | 156 | 157 | # [1.5.0](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/compare/v1.4.1...v1.5.0) (2017-11-13) 158 | 159 | 160 | ### Features 161 | 162 | * add notify option ([af6b36a](https://github.com/moxystudio/webpack-isomorphic-dev-middleware/commit/af6b36a)) 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-isomorphic-dev-middleware 2 | 3 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][codecov-image]][codecov-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] 4 | 5 | [npm-url]:https://npmjs.org/package/webpack-isomorphic-dev-middleware 6 | [npm-image]:https://img.shields.io/npm/v/webpack-isomorphic-dev-middleware.svg 7 | [downloads-image]:https://img.shields.io/npm/dm/webpack-isomorphic-dev-middleware.svg 8 | [travis-url]:https://travis-ci.org/moxystudio/webpack-isomorphic-dev-middleware 9 | [travis-image]:https://img.shields.io/travis/moxystudio/webpack-isomorphic-dev-middleware/master.svg 10 | [codecov-url]:https://codecov.io/gh/moxystudio/webpack-isomorphic-dev-middleware 11 | [codecov-image]:https://img.shields.io/codecov/c/github/moxystudio/webpack-isomorphic-dev-middleware/master.svg 12 | [david-dm-url]:https://david-dm.org/moxystudio/webpack-isomorphic-dev-middleware 13 | [david-dm-image]:https://img.shields.io/david/moxystudio/webpack-isomorphic-dev-middleware.svg 14 | [david-dm-dev-url]:https://david-dm.org/moxystudio/webpack-isomorphic-dev-middleware?type=dev 15 | [david-dm-dev-image]:https://img.shields.io/david/dev/moxystudio/webpack-isomorphic-dev-middleware.svg 16 | 17 | The [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware), but for isomorphic applications. 18 | 19 | ![Showcase](http://i.imgur.com/rgy7QcT.gif) 20 | 21 | 22 | ## Installation 23 | 24 | `$ npm install webpack-isomorphic-dev-middleware --save-dev` 25 | 26 | The current version works with webpack v2, v3 and v4. 27 | You might get a peer dependency warning when using webpack v2 or v3 but you may ignore it. 28 | 29 | 30 | ## Motivation 31 | 32 | Building applications powered by webpack with server-side rendering (isomorphic/universal apps) is hard. 33 | 34 | When making a production build, you must compile both the client and server. When developing, we want to rebuild the client & server and bring in the new compiled code without restarting/reloading the application. This is complex, especially setting up the development server. 35 | 36 | To make your development workflow easier to setup, `webpack-isomorphic-dev-middleware` offers an express middleware that: 37 | 38 | - Looks for code changes in both the client and the server and automatically compiles them 39 | - Optimizes compilation by using in-memory filesystem 40 | - Delays responses until the aggregated compiler finishes 41 | - Adds `isomorphic` to [res.locals](https://expressjs.com/en/api.html#res.locals), which includes the webpack stats and the methods exported in your server file 42 | - Offers beautiful compilation [reporting](https://github.com/moxystudio/webpack-isomorphic-compiler-reporter) into your terminal 43 | - Receive status through OS [notifications](https://github.com/moxystudio/webpack-sane-compiler-notifier) 44 | - Shows compilation errors in the browser on refresh, similar to the ones you get on the terminal 45 | 46 | 47 | ## Usage 48 | 49 | ```js 50 | const express = require('express'); 51 | const webpack = require('webpack'); 52 | const webpackIsomorphicDevMiddleware = require('webpack-isomorphic-dev-middleware'); 53 | const webpackHotMiddleware = require('webpack-hot-middleware'); 54 | 55 | const clientCompiler = webpack({ /* webpack client config */ }); 56 | const serverCompiler = webpack({ /* webpack server config */ }); 57 | const app = express(); 58 | 59 | // Serve any static files from the public folder 60 | app.use('/', express.static('public', { maxAge: 0, etag: false })); 61 | // Add the middleware that will wait for both client and server compilations to be ready 62 | app.use(webpackIsomorphicDevMiddleware(clientCompiler, serverCompiler)); 63 | // You may also add webpack-hot-middleware to provide hot module replacement to the client 64 | app.use(webpackHotMiddleware(clientCompiler, { quiet: true })); 65 | 66 | // Catch all route to attempt to render our isomorphic app 67 | app.get('*', (req, res, next) => { 68 | // res.isomorphic contains `compilation` & `exports` properties: 69 | // - `compilation` contains the webpack-isomorphic-compiler compilation result 70 | // - `exports` contains the server exports, usually one or more render functions 71 | const { render } = res.locals.isomorphic.exports; 72 | 73 | render({ req, res }) 74 | .catch((err) => setImmediate(() => next(err))); 75 | }); 76 | ``` 77 | 78 | The middleware function is flexible and supports various signatures: 79 | 80 | - Two separate webpack compilers 81 | 82 | ```js 83 | const clientCompiler = webpack({ /* webpack client config */ }); 84 | const serverCompiler = webpack({ /* webpack server config */ }); 85 | 86 | app.use(webpackIsomorphicDevMiddleware(clientCompiler, serverCompiler, { /* options */ })); 87 | ``` 88 | 89 | - A webpack multi-compiler where the first and second indexes belong to the client and server respectively, see https://webpack.js.org/api/node 90 | 91 | ```js 92 | const compiler = webpack([ 93 | /* webpack client config */, 94 | /* webpack server config */, 95 | ]); 96 | 97 | app.use(webpackIsomorphicDevMiddleware(compiler, { /* options */ })); 98 | ``` 99 | 100 | - A [webpack-isomorphic-compiler](https://github.com/moxystudio/webpack-isomorphic-compiler) that simplifies compiling isomorphic apps 101 | 102 | ```js 103 | const isomorphicCompiler = webpackIsomorphicCompiler( 104 | /* webpack client config */, 105 | /* webpack server config */ 106 | ); 107 | 108 | app.use(webpackIsomorphicDevMiddleware(isomorphicCompiler, { /* options */ })); 109 | ``` 110 | 111 | 112 | Available options: 113 | 114 | | Name | Description | Type | Default | 115 | | ------ | ------------- | -------- | ------- | 116 | | memoryFs | Either disable or enable in-memory filesystem (disabling decreases performance) | boolean | true | 117 | | watchOptions | Options to pass to webpack\'s watch | [object](https://webpack.js.org/configuration/watch/#watchoptions) | | 118 | | watchDelay | Delay calling webpack\'s watch for the given milliseconds | number | 0 | 119 | | report | Enables reporting | boolean/[object](https://github.com/moxystudio/webpack-isomorphic-compiler-reporter#available-options) | `{ stats: 'once' }` 120 | | notify | Report build status through OS notifications | boolean/[object](https://github.com/moxystudio/webpack-sane-compiler-notifier#available-options) | false | 121 | | headers | Headers to be sent when serving compiled files | object | `{ 'Cache-Control': 'max-age=0, must-revalidate' }` | 122 | | findServerAssetName | Finds the server asset to require from the Webpack stats | function | *first js asset from the first entrypoint* | 123 | 124 | By default, `findServerAsset` selects the first JavaScript asset from first entrypoint. If that doesn't suit your Webpack configuration, you may change it: 125 | 126 | ```js 127 | { 128 | // Finds the asset with the `server` word in its name 129 | findServerAssetName: (stats) => stats.assets 130 | .map((asset) => asset.name) 131 | .find((name) => /\bserver\b.+\.js$/.test(name)); 132 | } 133 | ``` 134 | 135 | 136 | ## Tests 137 | 138 | `$ npm test` 139 | `$ npm test -- --watch` during development 140 | 141 | 142 | ## License 143 | 144 | [MIT License](http://opensource.org/licenses/MIT) 145 | -------------------------------------------------------------------------------- /test/middleware.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const express = require('express'); 5 | const request = require('supertest'); 6 | const webpackIsomorphicDevMiddleware = require('../'); 7 | const createCompiler = require('./util/createCompiler'); 8 | const normalizeHtmlError = require('./util/normalizeHtmlError'); 9 | const configClientBasic = require('./configs/client-basic'); 10 | const configServerBasic = require('./configs/server-basic'); 11 | const configClientSyntaxError = require('./configs/client-syntax-error'); 12 | const configServerRuntimeError = require('./configs/server-runtime-error'); 13 | 14 | afterEach(() => createCompiler.teardown()); 15 | 16 | it('should wait for a successful compilation and call next()', () => { 17 | const app = express(); 18 | const compiler = createCompiler(configClientBasic, configServerBasic); 19 | 20 | app.use(webpackIsomorphicDevMiddleware(compiler, { 21 | report: false, 22 | })); 23 | 24 | return request(app) 25 | .get('/client.js') 26 | .expect(200) 27 | .expect(/Hello!/); 28 | }); 29 | 30 | it('should wait for a failed compilation and render the webpack stats', () => { 31 | const app = express(); 32 | const compiler = createCompiler(configClientSyntaxError, configServerBasic); 33 | 34 | app.use(webpackIsomorphicDevMiddleware(compiler, { 35 | report: false, 36 | })); 37 | 38 | return request(app) 39 | .get('/client.js') 40 | .expect(500) 41 | .expect((res) => { 42 | expect(normalizeHtmlError(res.text)).toMatchSnapshot(); 43 | }); 44 | }); 45 | 46 | it('should render error if an error occurred while reading the server file', () => { 47 | const app = express(); 48 | const compiler = createCompiler(configClientBasic, configServerRuntimeError); 49 | 50 | app.use(webpackIsomorphicDevMiddleware(compiler, { 51 | report: false, 52 | })); 53 | 54 | compiler.server.webpackCompiler.outputFileSystem.readFile = (...args) => { 55 | args[args.length - 1](new Error('Failed to read file')); 56 | }; 57 | 58 | return request(app) 59 | .get('/client.js') 60 | .expect(500) 61 | .expect((res) => { 62 | expect(normalizeHtmlError(res.text)).toMatchSnapshot(); 63 | }); 64 | }); 65 | 66 | it('should render error if the server file has a runtime error', () => { 67 | const app = express(); 68 | const compiler = createCompiler(configClientBasic, configServerRuntimeError); 69 | 70 | app.use(webpackIsomorphicDevMiddleware(compiler, { 71 | report: false, 72 | })); 73 | 74 | return request(app) 75 | .get('/client.js') 76 | .expect(500) 77 | .expect((res) => { 78 | expect(normalizeHtmlError(res.text)).toMatchSnapshot(); 79 | }); 80 | }); 81 | 82 | it('should call next(err) if not a middleware error', () => { 83 | const app = express(); 84 | const compiler = createCompiler(configClientBasic, configServerBasic); 85 | const contrivedError = new Error('foo'); 86 | 87 | app.use((req, res, next) => { 88 | next(contrivedError); 89 | }); 90 | app.use(webpackIsomorphicDevMiddleware(compiler, { 91 | report: false, 92 | })); 93 | app.use((err, req, res, next) => { // eslint-disable-line handle-callback-err, no-unused-vars 94 | expect(err).toBe(contrivedError); 95 | res.send(`Captured contrived error: ${err.message}`); 96 | }); 97 | 98 | return request(app) 99 | .get('/client.js') 100 | .expect(200) 101 | .expect('Captured contrived error: foo'); 102 | }); 103 | 104 | it('should set res.locals.isomorphicCompilation', async () => { 105 | const app = express(); 106 | const compiler = createCompiler(configClientBasic, configServerBasic); 107 | let isomorphicCompilation; 108 | let exports; 109 | 110 | app.use(webpackIsomorphicDevMiddleware(compiler, { 111 | report: false, 112 | })); 113 | 114 | app.get('*', (req, res) => { 115 | exports = res.locals.isomorphic.exports; 116 | isomorphicCompilation = res.locals.isomorphic.compilation; 117 | res.send('Yes it works!'); 118 | }); 119 | 120 | await request(app) 121 | .get('/') 122 | .expect(200) 123 | .expect('Yes it works!'); 124 | 125 | expect(isomorphicCompilation).toBeDefined(); 126 | expect(isomorphicCompilation).toHaveProperty('clientStats'); 127 | expect(isomorphicCompilation).toHaveProperty('serverStats'); 128 | 129 | expect(exports).toBeDefined(); 130 | expect(exports).toHaveProperty('render'); 131 | }); 132 | 133 | it('should cache the res.locals.isomorphicCompilation', async () => { 134 | const app = express(); 135 | const compiler = createCompiler(configClientBasic, configServerBasic); 136 | let isomorphicCompilation; 137 | 138 | app.use(webpackIsomorphicDevMiddleware(compiler, { 139 | report: false, 140 | })); 141 | 142 | app.get('*', (req, res) => { 143 | if (!isomorphicCompilation) { 144 | isomorphicCompilation = res.locals.isomorphicCompilation; 145 | } else { 146 | expect(res.locals.isomorphicCompilation).toBe(isomorphicCompilation); 147 | } 148 | 149 | res.send('Yes it works!'); 150 | }); 151 | 152 | const spy = jest.spyOn(compiler.server.webpackCompiler.outputFileSystem, 'readFile'); 153 | 154 | await request(app) 155 | .get('/') 156 | .expect(200) 157 | .expect('Yes it works!'); 158 | 159 | await request(app) 160 | .get('/') 161 | .expect(200) 162 | .expect('Yes it works!'); 163 | 164 | expect(spy.mock.calls).toHaveLength(1); 165 | }); 166 | 167 | it('should not re-require the server file if it has a runtime error', async () => { 168 | const app = express(); 169 | const compiler = createCompiler(configClientBasic, configServerRuntimeError); 170 | 171 | app.use(webpackIsomorphicDevMiddleware(compiler, { 172 | report: false, 173 | })); 174 | 175 | const spy = jest.spyOn(compiler.server.webpackCompiler.outputFileSystem, 'readFile'); 176 | 177 | let res = await request(app) 178 | .get('/') 179 | .expect(500); 180 | 181 | expect(normalizeHtmlError(res.text)).toMatchSnapshot(); 182 | 183 | res = await request(app) 184 | .get('/') 185 | .expect(500); 186 | 187 | expect(normalizeHtmlError(res.text)).toMatchSnapshot(); 188 | 189 | expect(spy.mock.calls).toHaveLength(1); 190 | expect(spy.mock.calls[0][0]).toMatch(/server\.js$/); 191 | }); 192 | 193 | it('should not use in-memory filesystem if options.memoryFs is false', async () => { 194 | const app = express(); 195 | const compiler = createCompiler(configClientBasic, configServerBasic); 196 | 197 | app.use(webpackIsomorphicDevMiddleware(compiler, { 198 | memoryFs: false, 199 | report: false, 200 | })); 201 | 202 | await request(app) 203 | .get('/client.js') 204 | .expect(200); 205 | 206 | expect(fs.existsSync(`${compiler.client.webpackConfig.output.path}/client.js`)).toBe(true); 207 | expect(fs.existsSync(`${compiler.server.webpackConfig.output.path}/server.js`)).toBe(true); 208 | }); 209 | 210 | it('should watch() with the specified options.watchDelay', (next) => { 211 | const compiler = createCompiler(configClientBasic, configServerBasic); 212 | const now = Date.now(); 213 | 214 | webpackIsomorphicDevMiddleware(compiler, { watchDelay: 100 }); 215 | 216 | compiler.watch = () => { 217 | expect(Date.now() - now).toBeGreaterThanOrEqual(100); 218 | next(); 219 | }; 220 | }); 221 | 222 | it('should set headers as specified in options.headers', () => { 223 | const app = express(); 224 | const compiler = createCompiler(configClientBasic, configServerBasic); 225 | 226 | app.use(webpackIsomorphicDevMiddleware(compiler, { 227 | report: false, 228 | headers: { 'X-FOO': 'bar' }, 229 | })); 230 | 231 | return request(app) 232 | .get('/client.js') 233 | .expect(200) 234 | .expect('X-FOO', 'bar') 235 | .expect('Cache-Control', 'max-age=0, must-revalidate') 236 | .expect(/Hello!/); 237 | }); 238 | 239 | it('should allow removing the default Cache-Control header via options.headers', () => { 240 | const app = express(); 241 | const compiler = createCompiler(configClientBasic, configServerBasic); 242 | 243 | app.use(webpackIsomorphicDevMiddleware(compiler, { 244 | report: false, 245 | headers: { 'Cache-Control': null }, 246 | })); 247 | 248 | return request(app) 249 | .get('/client.js') 250 | .expect(200) 251 | .expect((res) => { 252 | expect(res.headers).not.toHaveProperty('cache-control'); 253 | }) 254 | .expect(/Hello!/); 255 | }); 256 | -------------------------------------------------------------------------------- /test/__snapshots__/middleware.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should not re-require the server file if it has a runtime error 1`] = ` 4 | " 5 | 6 | 7 | 8 | webpack-isomorphic-dev-middleware error 9 | 44 | 45 | ReferenceError: foo is not defined 46 | [stack] 47 | 48 | The error above was thrown while trying to load the built server file: 49 | test/tmp/server.js 50 | " 51 | `; 52 | 53 | exports[`should not re-require the server file if it has a runtime error 2`] = ` 54 | " 55 | 56 | 57 | 58 | webpack-isomorphic-dev-middleware error 59 | 94 | 95 | ReferenceError: foo is not defined 96 | [stack] 97 | 98 | The error above was thrown while trying to load the built server file: 99 | test/tmp/server.js 100 | " 101 | `; 102 | 103 | exports[`should render error if an error occurred while reading the server file 1`] = ` 104 | " 105 | 106 | 107 | 108 | webpack-isomorphic-dev-middleware error 109 | 144 | 145 | Failed to read file 146 | [stack] 147 | 148 | The error above was thrown while trying to load the built server file: 149 | test/tmp/server.js 150 | " 151 | `; 152 | 153 | exports[`should render error if the server file has a runtime error 1`] = ` 154 | " 155 | 156 | 157 | 158 | webpack-isomorphic-dev-middleware error 159 | 194 | 195 | ReferenceError: foo is not defined 196 | [stack] 197 | 198 | The error above was thrown while trying to load the built server file: 199 | test/tmp/server.js 200 | " 201 | `; 202 | 203 | exports[`should wait for a failed compilation and render the webpack stats 1`] = ` 204 | " 205 | 206 | 207 | 208 | webpack-isomorphic-dev-middleware error 209 | 244 | 245 | Webpack compilation failed (client) 246 | 247 | ERROR in ./test/configs/files/syntax-error.js 248 | Module parse failed: Unexpected token (4:0) 249 | You may need an appropriate loader to handle this file type. 250 | | 251 | | console.log('Hello!' 252 | | 253 | " 254 | `; 255 | --------------------------------------------------------------------------------