Skip to content

Commit

Permalink
feat(server): add contentBasePublicPath option (#2150)
Browse files Browse the repository at this point in the history
Tell the server at what URL to serve `devServer.contentBase`. If there was a file `assets/manifest.json`, it would be served at `/serve-content-base-at-this-url/manifest.json`

__webpack.config.js__

```javascript
module.exports = {
  //...
  devServer: {
    contentBase: path.join(__dirname, 'assets'),
    contentBasePublicPath: '/serve-content-base-at-this-url'
  }
};
```

Now `webpack-dev-server` serves static files (`contentBase`) from root ignoring `publicPath`
Using added option we can serve static files from `publicPath`
This use case is needed in create-react-app, when want to serve all files from `publicPath`
doesn't matter in development or production

facebook/create-react-app#7259
  • Loading branch information
iamandrewluca authored and evilebottnawi committed Dec 5, 2019
1 parent bd6783a commit cee700d
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 5 deletions.
12 changes: 7 additions & 5 deletions lib/Server.js
Expand Up @@ -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(
Expand Down Expand Up @@ -376,25 +377,26 @@ class Server {
});
} else {
// route content request
this.app.get(
'*',
this.app.use(
contentBasePublicPath,
express.static(contentBase, this.options.staticOptions)
);
}
}

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));
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/options.json
Expand Up @@ -51,6 +51,9 @@
"compress": {
"type": "boolean"
},
"contentBasePublicPath": {
"type": "string"
},
"contentBase": {
"anyOf": [
{
Expand Down Expand Up @@ -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)",
Expand Down
3 changes: 3 additions & 0 deletions lib/utils/normalizeOptions.js
Expand Up @@ -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 = {
Expand Down
295 changes: 295 additions & 0 deletions 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);
});
});
});

0 comments on commit cee700d

Please sign in to comment.