diff --git a/README.md b/README.md index eb787f3fd..7022ac2c3 100644 --- a/README.md +++ b/README.md @@ -200,32 +200,64 @@ interact with the middleware at runtime: ### `close(callback)` -Instructs a webpack-dev-middleware instance to stop watching for file changes. +Instructs `webpack-dev-middleware` instance to stop watching for file changes. -### Parameters +#### Parameters -#### callback +##### `callback` Type: `Function` +Required: `No` A function executed once the middleware has stopped watching. -### `invalidate()` +```js +const express = require('express'); +const webpack = require('webpack'); +const compiler = webpack({ + /* Webpack configuration */ +}); +const middleware = require('webpack-dev-middleware'); +const instance = middleware(compiler); + +const app = new express(); + +app.use(instance); + +setTimeout(() => { + // Says `webpack` to stop watch changes + instance.close(); +}, 1000); +``` -Instructs a webpack-dev-middleware instance to recompile the bundle. -e.g. after a change to the configuration. +### `invalidate(callback)` + +Instructs `webpack-dev-middleware` instance to recompile the bundle, e.g. after a change to the configuration. + +#### Parameters + +##### `callback` + +Type: `Function` +Required: `No` + +A function executed once the middleware has invalidated. ```js +const express = require('express'); const webpack = require('webpack'); -const compiler = webpack({ ... }); +const compiler = webpack({ + /* Webpack configuration */ +}); const middleware = require('webpack-dev-middleware'); const instance = middleware(compiler); +const app = new express(); + app.use(instance); setTimeout(() => { - // After a short delay the configuration is changed and a banner plugin is added - // to the config + // After a short delay the configuration is changed and a banner plugin is added to the config new webpack.BannerPlugin('A new banner').apply(compiler); // Recompile the bundle with the banner plugin: @@ -238,21 +270,27 @@ setTimeout(() => { Executes a callback function when the compiler bundle is valid, typically after compilation. -### Parameters +#### Parameters -#### callback +##### `callback` Type: `Function` +Required: `No` -A function executed when the bundle becomes valid. If the bundle is -valid at the time of calling, the callback is executed immediately. +A function executed when the bundle becomes valid. +If the bundle is valid at the time of calling, the callback is executed immediately. ```js +const express = require('express'); const webpack = require('webpack'); -const compiler = webpack({ ... }); +const compiler = webpack({ + /* Webpack configuration */ +}); const middleware = require('webpack-dev-middleware'); const instance = middleware(compiler); +const app = new express(); + app.use(instance); instance.waitUntilValid(() => { @@ -260,6 +298,39 @@ instance.waitUntilValid(() => { }); ``` +### `getFilenameFromUrl(url)` + +Get filename from URL. + +#### Parameters + +##### `url` + +Type: `String` +Required: `Yes` + +URL for the requested file. + +```js +const express = require('express'); +const webpack = require('webpack'); +const compiler = webpack({ + /* Webpack configuration */ +}); +const middleware = require('webpack-dev-middleware'); +const instance = middleware(compiler); + +const app = new express(); + +app.use(instance); + +instance.waitUntilValid(() => { + const filename = instance.getFilenameFromUrl('/bundle.js'); + + console.log(`Filename is ${filename}`); +}); +``` + ## Known Issues ### Multiple Successive Builds @@ -289,13 +360,16 @@ process is finished with server-side rendering enabled._ Example Implementation: ```js +const express = require('express'); const webpack = require('webpack'); const compiler = webpack({ - // webpack options + /* Webpack configuration */ }); const isObject = require('is-object'); const middleware = require('webpack-dev-middleware'); +const app = new express(); + // This function makes server rendering of asset references consistent with different webpack chunk/entry configurations function normalizeAssets(assets) { if (isObject(assets)) { diff --git a/src/index.js b/src/index.js index e5edae42b..714df3350 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import { validate } from 'schema-utils'; import mime from 'mime-types'; import middleware from './middleware'; +import getFilenameFromUrl from './utils/getFilenameFromUrl'; import setupHooks from './utils/setupHooks'; import setupWriteToDisk from './utils/setupWriteToDisk'; import setupOutputFileSystem from './utils/setupOutputFileSystem'; @@ -76,17 +77,22 @@ export default function wdm(compiler, options = {}) { const instance = middleware(context); // API + instance.getFilenameFromUrl = (url) => getFilenameFromUrl(context, url); + instance.waitUntilValid = (callback = noop) => { ready(context, callback); }; + instance.invalidate = (callback = noop) => { ready(context, callback); context.watching.invalidate(); }; + instance.close = (callback = noop) => { context.watching.close(callback); }; + instance.context = context; return instance; diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index c9af30ee6..a802f9f48 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -12,17 +12,18 @@ export default function getFilenameFromUrl(context, url) { const { options } = context; const paths = getPaths(context); - let filename; + let foundFilename; let urlObject; try { // The `url` property of the `request` is contains only `pathname`, `search` and `hash` urlObject = memoizedParse(url, false, true); } catch (_ignoreError) { - return filename; + return; } for (const { publicPath, outputPath } of paths) { + let filename; let publicPathObject; try { @@ -62,6 +63,8 @@ export default function getFilenameFromUrl(context, url) { } if (fsStats.isFile()) { + foundFilename = filename; + break; } else if ( fsStats.isDirectory() && @@ -83,11 +86,14 @@ export default function getFilenameFromUrl(context, url) { } if (fsStats.isFile()) { + foundFilename = filename; + break; } } } } - return filename; + // eslint-disable-next-line consistent-return + return foundFilename; } diff --git a/test/api.test.js b/test/api.test.js index bf8fec3be..cae872962 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -1,3 +1,5 @@ +import path from 'path'; + import express from 'express'; import connect from 'connect'; import webpack, { Stats } from 'webpack'; @@ -7,6 +9,8 @@ import middleware from '../src'; import getCompiler from './helpers/getCompiler'; import getCompilerHooks from './helpers/getCompilerHooks'; import webpackConfig from './fixtures/webpack.config'; +import webpackPublicPathConfig from './fixtures/webpack.public-path.config'; +import webpackMultiConfig from './fixtures/webpack.array.config'; // Suppress unnecessary stats output global.console.log = jest.fn(); @@ -372,6 +376,283 @@ describe.each([ }); }); + describe('getFilenameFromUrl method', () => { + describe('should work', () => { + beforeEach((done) => { + compiler = getCompiler(webpackConfig); + + instance = middleware(compiler); + + app = framework(); + app.use(instance); + + listen = app.listen((error) => { + if (error) { + return done(error); + } + + return done(); + }); + }); + + afterEach((done) => { + if (instance.context.watching.closed) { + if (listen) { + listen.close(done); + } else { + done(); + } + + return; + } + + instance.close(() => { + if (listen) { + listen.close(done); + } else { + done(); + } + }); + }); + + it('should work', (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl('/bundle.js')).toBe( + path.join(webpackConfig.output.path, '/bundle.js') + ); + expect(instance.getFilenameFromUrl('/')).toBe( + path.join(webpackConfig.output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/index.html')).toBe( + path.join(webpackConfig.output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/svg.svg')).toBe( + path.join(webpackConfig.output.path, '/svg.svg') + ); + expect( + instance.getFilenameFromUrl('/unknown.unknown') + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/unknown/unknown.unknown') + ).toBeUndefined(); + + done(); + }); + }); + }); + + describe('should work when the "index" option disabled', () => { + beforeEach((done) => { + compiler = getCompiler(webpackConfig); + + instance = middleware(compiler, { index: false }); + + app = framework(); + app.use(instance); + + listen = app.listen((error) => { + if (error) { + return done(error); + } + + return done(); + }); + }); + + afterEach((done) => { + if (instance.context.watching.closed) { + if (listen) { + listen.close(done); + } else { + done(); + } + + return; + } + + instance.close(() => { + if (listen) { + listen.close(done); + } else { + done(); + } + }); + }); + + it('should work', (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl('/bundle.js')).toBe( + path.join(webpackConfig.output.path, '/bundle.js') + ); + // eslint-disable-next-line no-undefined + expect(instance.getFilenameFromUrl('/')).toBe(undefined); + expect(instance.getFilenameFromUrl('/index.html')).toBe( + path.join(webpackConfig.output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/svg.svg')).toBe( + path.join(webpackConfig.output.path, '/svg.svg') + ); + expect( + instance.getFilenameFromUrl('/unknown.unknown') + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/unknown/unknown.unknown') + ).toBeUndefined(); + + done(); + }); + }); + }); + + describe('should work with the "publicPath" option', () => { + beforeEach((done) => { + compiler = getCompiler(webpackPublicPathConfig); + + instance = middleware(compiler); + + app = framework(); + app.use(instance); + + listen = app.listen((error) => { + if (error) { + return done(error); + } + + return done(); + }); + }); + + afterEach((done) => { + if (instance.context.watching.closed) { + if (listen) { + listen.close(done); + } else { + done(); + } + + return; + } + + instance.close(() => { + if (listen) { + listen.close(done); + } else { + done(); + } + }); + }); + + it('should work', (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl('/public/path/bundle.js')).toBe( + path.join(webpackPublicPathConfig.output.path, '/bundle.js') + ); + expect(instance.getFilenameFromUrl('/public/path/')).toBe( + path.join(webpackPublicPathConfig.output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/public/path/index.html')).toBe( + path.join(webpackPublicPathConfig.output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/public/path/svg.svg')).toBe( + path.join(webpackPublicPathConfig.output.path, '/svg.svg') + ); + + expect(instance.getFilenameFromUrl('/')).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/unknown.unknown') + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/unknown/unknown.unknown') + ).toBeUndefined(); + + done(); + }); + }); + }); + + describe('should work in multi compiler mode', () => { + beforeEach((done) => { + compiler = getCompiler(webpackMultiConfig); + + instance = middleware(compiler); + + app = framework(); + app.use(instance); + + listen = app.listen((error) => { + if (error) { + return done(error); + } + + return done(); + }); + }); + + afterEach((done) => { + if (instance.context.watching.closed) { + if (listen) { + listen.close(done); + } else { + done(); + } + + return; + } + + instance.close(() => { + if (listen) { + listen.close(done); + } else { + done(); + } + }); + }); + + it('should work', (done) => { + instance.waitUntilValid(() => { + expect(instance.getFilenameFromUrl('/static-one/bundle.js')).toBe( + path.join(webpackMultiConfig[0].output.path, '/bundle.js') + ); + expect(instance.getFilenameFromUrl('/static-one/')).toBe( + path.join(webpackMultiConfig[0].output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/static-one/index.html')).toBe( + path.join(webpackMultiConfig[0].output.path, '/index.html') + ); + expect(instance.getFilenameFromUrl('/static-one/svg.svg')).toBe( + path.join(webpackMultiConfig[0].output.path, '/svg.svg') + ); + expect( + instance.getFilenameFromUrl('/static-one/unknown.unknown') + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/static-one/unknown/unknown.unknown') + ).toBeUndefined(); + + expect(instance.getFilenameFromUrl('/static-two/bundle.js')).toBe( + path.join(webpackMultiConfig[1].output.path, '/bundle.js') + ); + expect( + instance.getFilenameFromUrl('/static-two/unknown.unknown') + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/static-two/unknown/unknown.unknown') + ).toBeUndefined(); + + expect(instance.getFilenameFromUrl('/')).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/static-one/unknown.unknown') + ).toBeUndefined(); + expect( + instance.getFilenameFromUrl('/static-one/unknown/unknown.unknown') + ).toBeUndefined(); + + done(); + }); + }); + }); + }); + describe('close method', () => { beforeEach((done) => { compiler = getCompiler(webpackConfig); diff --git a/test/fixtures/webpack.public-path.config.js b/test/fixtures/webpack.public-path.config.js index e99686fd1..12ba710c7 100644 --- a/test/fixtures/webpack.public-path.config.js +++ b/test/fixtures/webpack.public-path.config.js @@ -5,12 +5,21 @@ const path = require('path'); module.exports = { mode: 'development', context: path.resolve(__dirname), - entry: './simple.js', + entry: './foo.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, '../outputs/public-path'), publicPath: '/public/path/', }, + module: { + rules: [ + { + test: /\.(svg|html)$/, + loader: 'file-loader', + options: { name: '[name].[ext]' }, + }, + ], + }, infrastructureLogging: { level: 'none' }, diff --git a/test/utils/getFilenameFromUrl.test.js b/test/utils/getFilenameFromUrl.test.js deleted file mode 100644 index 7d8c7c4bf..000000000 --- a/test/utils/getFilenameFromUrl.test.js +++ /dev/null @@ -1,154 +0,0 @@ -import path from 'path'; - -import express from 'express'; - -import middleware from '../../src'; -import getFilenameFromUrl from '../../src/utils/getFilenameFromUrl'; -import getCompiler from '../helpers/getCompiler'; -import listenAndCompile from '../helpers/listenAndCompile'; -import webpackSimpleConfig from '../fixtures/webpack.simple.config'; -import webpackPublicPathConfig from '../fixtures/webpack.public-path.config'; -import webpackMultiConfig from '../fixtures/webpack.array.config'; - -// Suppress unnecessary stats output -global.console.log = jest.fn(); - -describe('getFilenameFromUrl', () => { - const configs = [ - { - title: 'simple config with path /', - config: webpackSimpleConfig, - middlewareConfig: {}, - url: '/', - expected: path.resolve(__dirname, '../outputs/simple/index.html'), - }, - { - title: 'simple config with path /index.html', - config: webpackSimpleConfig, - middlewareConfig: {}, - url: '/index.html', - expected: path.resolve(__dirname, '../outputs/simple/index.html'), - }, - { - title: 'simple config with path /path', - config: webpackSimpleConfig, - middlewareConfig: {}, - url: '/path', - expected: path.resolve(__dirname, '../outputs/simple/path'), - }, - { - title: 'simple config with path /path/file.html', - config: webpackSimpleConfig, - middlewareConfig: {}, - url: '/path/file.html', - expected: path.resolve(__dirname, '../outputs/simple/path/file.html'), - }, - { - title: 'simple config with index false and path /', - config: webpackSimpleConfig, - middlewareConfig: { - index: false, - }, - url: '/', - expected: path.resolve(__dirname, '../outputs/simple'), - }, - { - title: 'simple config with index file.html and path /', - config: webpackSimpleConfig, - middlewareConfig: { - index: 'file.html', - }, - url: '/', - expected: path.resolve(__dirname, '../outputs/simple/file.html'), - }, - - { - title: 'publicPath config with path /', - config: webpackPublicPathConfig, - middlewareConfig: {}, - url: '/', - expected: null, - }, - { - title: 'publicPath config with path /public/path/', - config: webpackPublicPathConfig, - middlewareConfig: {}, - url: '/public/path/', - expected: path.resolve(__dirname, '../outputs/public-path/index.html'), - }, - { - title: 'publicPath config with path /public/path/more/file.html', - config: webpackPublicPathConfig, - middlewareConfig: {}, - url: '/public/path/more/file.html', - expected: path.resolve( - __dirname, - '../outputs/public-path/more/file.html' - ), - }, - - { - title: 'multi config with path /', - config: webpackMultiConfig, - middlewareConfig: {}, - url: '/', - expected: null, - }, - { - title: 'multi config with path /static-one/', - config: webpackMultiConfig, - middlewareConfig: {}, - url: '/static-one/', - expected: path.resolve(__dirname, '../outputs/array/js1/index.html'), - }, - { - title: 'multi config with path /static-two/', - config: webpackMultiConfig, - middlewareConfig: {}, - url: '/static-two/', - expected: path.resolve(__dirname, '../outputs/array/js2/index.html'), - }, - ]; - - configs.forEach((config) => { - describe(config.title, () => { - let instance; - let listen; - let app; - let compiler; - - beforeEach((done) => { - compiler = getCompiler(config.config); - - instance = middleware(compiler, config.middlewareConfig); - - app = express(); - app.use(instance); - - listen = listenAndCompile(app, compiler, done); - }); - - afterEach((done) => { - if (instance) { - instance.close(); - } - - if (listen) { - listen.close(done); - } else { - done(); - } - }); - - it('should return correct filename from url', () => { - const filename = getFilenameFromUrl(instance.context, config.url); - const { expected } = config; - if (expected) { - expect(filename).toEqual(expected); - } else { - expect(filename).toBeUndefined(); - } - }); - }); - }); -});