diff --git a/lib/Server.js b/lib/Server.js index 809b214f24..4edba202de 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -334,10 +334,11 @@ class Server { setupStaticFeature() { const contentBase = this.options.contentBase; + const contentBasePublicPath = this.options.contentBasePublicPath; if (Array.isArray(contentBase)) { contentBase.forEach((item) => { - this.app.get('*', express.static(item)); + this.app.use(contentBasePublicPath, express.static(item)); }); } else if (isAbsoluteUrl(String(contentBase))) { this.log.warn( @@ -376,8 +377,8 @@ class Server { }); } else { // route content request - this.app.get( - '*', + this.app.use( + contentBasePublicPath, express.static(contentBase, this.options.staticOptions) ); } @@ -385,16 +386,17 @@ class Server { setupServeIndexFeature() { const contentBase = this.options.contentBase; + const contentBasePublicPath = this.options.contentBasePublicPath; if (Array.isArray(contentBase)) { contentBase.forEach((item) => { - this.app.get('*', serveIndex(item)); + this.app.use(contentBasePublicPath, serveIndex(item)); }); } else if ( typeof contentBase !== 'number' && !isAbsoluteUrl(String(contentBase)) ) { - this.app.get('*', serveIndex(contentBase)); + this.app.use(contentBasePublicPath, serveIndex(contentBase)); } } diff --git a/lib/options.json b/lib/options.json index 2842b7e30d..0936c4f5f9 100644 --- a/lib/options.json +++ b/lib/options.json @@ -51,6 +51,9 @@ "compress": { "type": "boolean" }, + "contentBasePublicPath": { + "type": "string" + }, "contentBase": { "anyOf": [ { @@ -460,6 +463,7 @@ "quiet": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverquiet-)", "reporter": "should be {Function} (https://github.com/webpack/webpack-dev-middleware#reporter)", "requestCert": "should be {Boolean}", + "contentBasePublicPath": "should be {String} (https://webpack.js.org/configuration/dev-server/#devservercontentbasepublicpath)", "serveIndex": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverserveindex)", "serverSideRender": "should be {Boolean} (https://github.com/webpack/webpack-dev-middleware#serversiderender)", "setup": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserversetup)", diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 885f365687..960d491416 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -9,6 +9,9 @@ function normalizeOptions(compiler, options) { options.contentBase = options.contentBase !== undefined ? options.contentBase : process.cwd(); + // Setup default value + options.contentBasePublicPath = options.contentBasePublicPath || '/'; + // normalize transportMode option if (options.transportMode === undefined) { options.transportMode = { diff --git a/test/server/contentBasePublicPath-option.test.js b/test/server/contentBasePublicPath-option.test.js new file mode 100644 index 0000000000..b9eb5c3fa6 --- /dev/null +++ b/test/server/contentBasePublicPath-option.test.js @@ -0,0 +1,295 @@ +'use strict'; + +const path = require('path'); +const request = require('supertest'); +const testServer = require('../helpers/test-server'); +const config = require('../fixtures/contentbase-config/webpack.config'); +const port = require('../ports-map')['contentBase-option']; + +const contentBasePublic = path.resolve( + __dirname, + '../fixtures/contentbase-config/public' +); +const contentBaseOther = path.resolve( + __dirname, + '../fixtures/contentbase-config/other' +); + +const contentBasePublicPath = '/serve-content-base-at-this-url'; + +describe('contentBasePublicPath option', () => { + let server; + let req; + + describe('to directory', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: contentBasePublic, + contentBasePublicPath, + watchContentBase: true, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('Request to index', (done) => { + req.get(`${contentBasePublicPath}/`).expect(200, /Heyo/, done); + }); + + it('Request to other file', (done) => { + req + .get(`${contentBasePublicPath}/other.html`) + .expect(200, /Other html/, done); + }); + }); + + describe('test listing files in folders without index.html using the option serveIndex:false', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: contentBasePublic, + contentBasePublicPath, + watchContentBase: true, + serveIndex: false, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it("shouldn't list the files inside the assets folder (404)", (done) => { + req.get(`${contentBasePublicPath}/assets/`).expect(404, done); + }); + + it('should show Heyo. because bar has index.html inside it (200)', (done) => { + req.get(`${contentBasePublicPath}/bar/`).expect(200, /Heyo/, done); + }); + }); + + describe('test listing files in folders without index.html using the option serveIndex:true', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: contentBasePublic, + contentBasePublicPath, + watchContentBase: true, + serveIndex: true, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('should list the files inside the assets folder (200)', (done) => { + req.get(`${contentBasePublicPath}/assets/`).expect(200, done); + }); + + it('should show Heyo. because bar has index.html inside it (200)', (done) => { + req.get(`${contentBasePublicPath}/bar/`).expect(200, /Heyo/, done); + }); + }); + + describe('test listing files in folders without index.html using the option serveIndex default (true)', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: contentBasePublic, + contentBasePublicPath, + watchContentBase: true, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('should list the files inside the assets folder (200)', (done) => { + req.get(`${contentBasePublicPath}/assets/`).expect(200, done); + }); + + it('should show Heyo. because bar has index.html inside it (200)', (done) => { + req.get(`${contentBasePublicPath}/bar/`).expect(200, /Heyo/, done); + }); + }); + + describe('to directories', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: [contentBasePublic, contentBaseOther], + contentBasePublicPath, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('Request to first directory', (done) => { + req.get(`${contentBasePublicPath}/`).expect(200, /Heyo/, done); + }); + + it('Request to second directory', (done) => { + req.get(`${contentBasePublicPath}/foo.html`).expect(200, /Foo!/, done); + }); + }); + + describe('default to PWD', () => { + beforeAll((done) => { + jest.spyOn(process, 'cwd').mockImplementation(() => contentBasePublic); + + server = testServer.start(config, { contentBasePublicPath }, done); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('Request to page', (done) => { + req.get(`${contentBasePublicPath}/other.html`).expect(200, done); + }); + }); + + describe('disable', () => { + beforeAll((done) => { + // This is a somewhat weird test, but it is important that we mock + // the PWD here, and test if /other.html in our "fake" PWD really is not requested. + jest.spyOn(process, 'cwd').mockImplementation(() => contentBasePublic); + + server = testServer.start( + config, + { + contentBase: false, + contentBasePublicPath, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('Request to page', (done) => { + req.get(`${contentBasePublicPath}/other.html`).expect(404, done); + }); + }); + + describe('Content type', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: [contentBasePublic], + contentBasePublicPath, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(() => { + done(); + }); + }); + + it('Request foo.wasm', (done) => { + req + .get(`${contentBasePublicPath}/foo.wasm`) + .expect('Content-Type', 'application/wasm', done); + }); + }); + + describe('to ignore other methods than GET and HEAD', () => { + beforeAll((done) => { + server = testServer.start( + config, + { + contentBase: contentBasePublic, + contentBasePublicPath, + watchContentBase: true, + port, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + testServer.close(done); + }); + + it('GET request', (done) => { + req.get(`${contentBasePublicPath}/`).expect(200, done); + }); + + it('HEAD request', (done) => { + req.head(`${contentBasePublicPath}/`).expect(200, done); + }); + + it('POST request', (done) => { + req.post(`${contentBasePublicPath}/`).expect(405, done); + }); + + it('PUT request', (done) => { + req.put(`${contentBasePublicPath}/`).expect(405, done); + }); + + it('DELETE request', (done) => { + req.delete(`${contentBasePublicPath}/`).expect(405, done); + }); + + it('PATCH request', (done) => { + req.patch(`${contentBasePublicPath}/`).expect(405, done); + }); + }); +}); diff --git a/test/server/utils/__snapshots__/normalizeOptions.test.js.snap b/test/server/utils/__snapshots__/normalizeOptions.test.js.snap index 5741d85d9a..00b8226a16 100644 --- a/test/server/utils/__snapshots__/normalizeOptions.test.js.snap +++ b/test/server/utils/__snapshots__/normalizeOptions.test.js.snap @@ -6,6 +6,7 @@ Object { "/path/to/dist1", "/path/to/dist2", ], + "contentBasePublicPath": "/", "transportMode": Object { "client": "sockjs", "server": "sockjs", @@ -17,6 +18,18 @@ Object { exports[`normalizeOptions contentBase string should set correct options 1`] = ` Object { "contentBase": "/path/to/dist", + "contentBasePublicPath": "/", + "transportMode": Object { + "client": "sockjs", + "server": "sockjs", + }, + "watchOptions": Object {}, +} +`; + +exports[`normalizeOptions contentBasePublicPath string should set correct options 1`] = ` +Object { + "contentBasePublicPath": "/content-base-public-path", "transportMode": Object { "client": "sockjs", "server": "sockjs", @@ -27,6 +40,7 @@ Object { exports[`normalizeOptions no options should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "sockjs", "server": "sockjs", @@ -37,6 +51,7 @@ Object { exports[`normalizeOptions transportMode custom client path should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "/path/to/custom/client/", "server": "sockjs", @@ -47,6 +62,7 @@ Object { exports[`normalizeOptions transportMode custom server class should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "sockjs", "server": [Function], @@ -57,6 +73,7 @@ Object { exports[`normalizeOptions transportMode custom server path should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "sockjs", "server": "/path/to/custom/server/", @@ -67,6 +84,7 @@ Object { exports[`normalizeOptions transportMode sockjs string should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "sockjs", "server": "sockjs", @@ -77,6 +95,7 @@ Object { exports[`normalizeOptions transportMode ws object should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "ws", "server": "ws", @@ -87,6 +106,7 @@ Object { exports[`normalizeOptions transportMode ws string should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "ws", "server": "ws", @@ -97,6 +117,7 @@ Object { exports[`normalizeOptions watchOptions should set correct options 1`] = ` Object { + "contentBasePublicPath": "/", "transportMode": Object { "client": "sockjs", "server": "sockjs", diff --git a/test/server/utils/normalizeOptions.test.js b/test/server/utils/normalizeOptions.test.js index 1490b36088..950b1975e2 100644 --- a/test/server/utils/normalizeOptions.test.js +++ b/test/server/utils/normalizeOptions.test.js @@ -94,6 +94,14 @@ describe('normalizeOptions', () => { }, optionsResults: null, }, + { + title: 'contentBasePublicPath string', + multiCompiler: false, + options: { + contentBasePublicPath: '/content-base-public-path', + }, + optionsResults: null, + }, ]; cases.forEach((data) => {