diff --git a/README.md b/README.md index aaac6fc1e..002e80ba8 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,13 @@ The public path that the middleware is bound to. _Best Practice: use the same `publicPath` defined in your webpack config. For more information about `publicPath`, please see [the webpack documentation](https://webpack.js.org/guides/public-path)._ +### historyApiFallback + +type: `Boolean` +Default: `false` + +When using the HTML5 History API, the index.html page will likely have to be served in place of any 404 responses. Enable historyApiFallback by setting it to true + ### stats Type: `Boolean|String|Object` diff --git a/src/index.js b/src/index.js index 0d6d6dec0..8cff67ecb 100644 --- a/src/index.js +++ b/src/index.js @@ -45,7 +45,7 @@ const noop = () => {}; */ /** - * @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, readFileSync?: import("fs").readFileSync }} OutputFileSystem + * @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, existsSync?: import("fs").existsSync, readFileSync?: import("fs").readFileSync }} OutputFileSystem */ /** @typedef {ReturnType} Logger */ @@ -88,6 +88,7 @@ const noop = () => {}; * @property {boolean} [serverSideRender] * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] + * @property {boolean | undefined} [historyApiFallback] */ /** diff --git a/src/options.json b/src/options.json index 0fa043a38..315fccbfd 100644 --- a/src/options.json +++ b/src/options.json @@ -72,6 +72,10 @@ } ] }, + "historyApiFallback": { + "description": "When using the HTML5 History API, the index.html page will likely have to be served in place of any 404 responses. Enable historyApiFallback by setting it to true", + "type": "boolean" + }, "stats": { "description": "Stats options object or preset name.", "link": "https://github.com/webpack/webpack-dev-middleware#stats", diff --git a/src/utils/getFilenameFromUrl.js b/src/utils/getFilenameFromUrl.js index 849a3bcb8..ab9b64575 100644 --- a/src/utils/getFilenameFromUrl.js +++ b/src/utils/getFilenameFromUrl.js @@ -95,8 +95,15 @@ function getFilenameFromUrl(context, url) { filename = path.join(outputPath, querystring.unescape(pathname)); } - let fsStats; + if ( + context.outputFileSystem.existsSync && + !context.outputFileSystem.existsSync(filename) && + options.historyApiFallback + ) { + filename = path.join(outputPath); + } + let fsStats; try { fsStats = /** @type {import("fs").statSync} */ diff --git a/test/middleware.test.js b/test/middleware.test.js index 4135765dd..534582795 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -3040,6 +3040,27 @@ describe.each([ }); }); + describe("historyApiFallback option", () => { + describe("index.html page will likely have to be served in place of any 404 responses", () => { + beforeAll((done) => { + const compiler = getCompiler(webpackConfig); + + instance = middleware(compiler, { historyApiFallback: true }); + + app = framework(); + app.use(instance); + + listen = listenShorthand(done); + }); + + afterAll(close); + + it('should return the "200" code for the "GET" request', (done) => { + request(app).get("/foo/bar/baz").expect(200, done); + }); + }); + }); + describe("serverSideRender option", () => { let locals; diff --git a/test/validation-options.test.js b/test/validation-options.test.js index b1e8e49ee..58d7e286a 100644 --- a/test/validation-options.test.js +++ b/test/validation-options.test.js @@ -39,6 +39,10 @@ describe("validation", () => { success: ["/foo", "", "auto", () => "/public/path"], failure: [false], }, + historyApiFallback: { + success: [true], + failure: [], + }, serverSideRender: { success: [true], failure: ["foo", 0], diff --git a/types/index.d.ts b/types/index.d.ts index 152459637..ee8ee80dc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -28,7 +28,7 @@ export = wdm; * @typedef {ReturnType} MultiWatching */ /** - * @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, readFileSync?: import("fs").readFileSync }} OutputFileSystem + * @typedef {Compiler["outputFileSystem"] & { createReadStream?: import("fs").createReadStream, statSync?: import("fs").statSync, lstat?: import("fs").lstat, existsSync?: import("fs").existsSync, readFileSync?: import("fs").readFileSync }} OutputFileSystem */ /** @typedef {ReturnType} Logger */ /** @@ -66,6 +66,7 @@ export = wdm; * @property {boolean} [serverSideRender] * @property {OutputFileSystem} [outputFileSystem] * @property {boolean | string} [index] + * @property {boolean | undefined} [historyApiFallback] */ /** * @template {IncomingMessage} RequestInternal @@ -172,6 +173,7 @@ type Options< serverSideRender?: boolean | undefined; outputFileSystem?: OutputFileSystem | undefined; index?: string | boolean | undefined; + historyApiFallback?: boolean | undefined; }; type API< RequestInternal extends import("http").IncomingMessage, @@ -204,6 +206,7 @@ type OutputFileSystem = Compiler["outputFileSystem"] & { createReadStream?: typeof import("fs").createReadStream; statSync?: import("fs").StatSyncFn; lstat?: typeof import("fs").lstat; + existsSync?: typeof import("fs").existsSync; readFileSync?: typeof import("fs").readFileSync; }; type Logger = ReturnType;