Skip to content

feat(middleware): add content option (options.content.filter, options.content.transform) #336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 56 additions & 25 deletions lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = function wrapper(context) {
return new Promise(((resolve) => {
handleRequest(context, filename, processRequest, req);
function processRequest() {
let notFound;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please elaborate further on the need for this

try {
let stat = context.fs.statSync(filename);

Expand All @@ -60,37 +61,67 @@ module.exports = function wrapper(context) {
throw new DevMiddlewareError('next');
}
}
notFound = false;
} catch (e) {
return resolve(goNext());
notFound = true;
}

// server content
let content = context.fs.readFileSync(filename);
content = handleRangeHeaders(content, req, res);

let contentType = mime.getType(filename);

// do not add charset to WebAssembly files, otherwise compileStreaming will fail in the client
if (!/\.wasm$/.test(filename)) {
contentType += '; charset=UTF-8';
const currentRequest = Object.assign({}, {
Copy link
Member

@michael-ciniawsky michael-ciniawsky Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const currentRequest => const request (we don't have different/multiple request(s) in this context)

url: req.url,
method: req.method,
headers: req.headers,
filename,
notFound
});
if (context.options && context.options.content && context.options.content.filter) {
if (!context.options.content.filter(currentRequest)) {
return goNext();
}
}
const defaultTransform = (localRequest, fs) => new Promise((localResolve) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why localRequest && localResolve instead of just req(uest) && resolve ? If it is because of the linter feel free to add a e.g /* eslint-disable no-shadow */ to the top of the file :)

if (notFound) {
return localResolve(null);
}
fs.readFile(localRequest.filename, (err, content) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a problem that the readFileSync was replaced by an async one?

Copy link
Member

@michael-ciniawsky michael-ciniawsky Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't say to 💯, but async I/O should be preferred. Maybe add a comment that it was changed in case we need to (temp) revert this for some reason :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs is a in memory filesystem so it doesn't matter

if (err) {
localResolve(null);
} else {
localResolve(content);
}
});
});
const optionTransform = (context.options && context.options.content) ? context.options.content.transform : null;
Copy link
Member

@michael-ciniawsky michael-ciniawsky Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm I not sure 💯 here, but something in the direction of

const { options, fs  } = context

// readFile -> Object { request, content, fs }
const readFile = (request, fs) => new Promise((resolve) => {
   ...
   return { request, content, fs }
})

// transform -> Promise<String|?Buffer>
const transform = (options.content && options.content.transform) 
   ? options.content.transform
   : ({ request, content, fs }) => content // default


return readFile(request, fs)
   .then(transform)
   .then((content) => { ... })

maybe, but definitely open for suggestions

return defaultTransform(currentRequest, context.fs)
.then(content => ((!optionTransform) ? content : optionTransform(currentRequest, content, context.fs)))
.then((content) => {
if (content == null) {
return Promise.reject();
Copy link
Member

@michael-ciniawsky michael-ciniawsky Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we better reject with an {Error} here ?

if (content == null) {
   const err = new Error('<message>')

   return Promise.reject(err)
}

}
content = handleRangeHeaders(content, req, res);
let contentType = mime.getType(currentRequest.filename);

// do not add charset to WebAssembly files, otherwise compileStreaming will fail in the client
if (!/\.wasm$/.test(currentRequest.filename)) {
contentType += '; charset=UTF-8';
}

res.setHeader('Content-Type', contentType);
res.setHeader('Content-Length', content.length);
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Length', content.length);

const { headers } = context.options;
if (headers) {
for (const name in headers) {
if ({}.hasOwnProperty.call(headers, name)) {
res.setHeader(name, context.options.headers[name]);
const { headers } = context.options;
if (headers) {
for (const name in headers) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name => header

if ({}.hasOwnProperty.call(headers, name)) {
res.setHeader(name, context.options.headers[name]);
}
}
}
}
}
// Express automatically sets the statusCode to 200, but not all servers do (Koa).
res.statusCode = res.statusCode || 200;
if (res.send) res.send(content);
else res.end(content);
resolve();
// Express automatically sets the statusCode to 200, but not all servers do (Koa).
res.statusCode = res.statusCode || 200;
if (res.send) res.send(content);
else res.end(content);
resolve();
})
.catch(goNext);
}
}));
};
Expand Down
108 changes: 108 additions & 0 deletions test/tests/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,114 @@ describe('Server', () => {
});
});

describe('filters', () => {
before((done) => {
app = express();
const compiler = webpack(webpackConfig);
instance = middleware(compiler, {
stats: 'errors-only',
content: {
filter(currentRequest) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentRequest => res || request

let result = true;
if (/.js$/.test(currentRequest.filename)) {
result = false;
}
return result;
}
},
logLevel,
publicPath: '/public/'
});
app.use(instance);
listen = listenShorthand(done);
});
after(close);

it('GET request to bundle file with /.js$/ filtered', (done) => {
request(app).get('/public/bundle.js')
.expect(404, done);
});

it('request to image with /.js$/ filtered', (done) => {
request(app).get('/public/svg.svg')
.expect('Content-Type', 'image/svg+xml; charset=UTF-8')
.expect('Content-Length', '4778')
.expect(200, done);
});
});

describe('content transform', () => {
before((done) => {
app = express();
const compiler = webpack(webpackConfig);
instance = middleware(compiler, {
stats: 'errors-only',
content: {
transform(currentRequest, content, memoryFileSystem) {
return new Promise((resolve, reject) => {
if (currentRequest.url === '/public/') {
resolve(Buffer.from(content.toString().replace(/My/, 'Modified')));
} else if (currentRequest.url === '/public/001/aliased-bundle.js') {
currentRequest.filename = '/bundle.js';
currentRequest.notFound = false;
memoryFileSystem.readFile(currentRequest.filename, (err, cont) => {
if (err) {
reject();
} else {
resolve(cont);
}
});
} else {
resolve(content);
}
});
}
},
logLevel,
publicPath: '/public/'
});
app.use(instance);
listen = listenShorthand(done);
});

after(close);

it('GET request to bundle file, with transform rules on the /public/ URL', (done) => {
request(app).get('/public/bundle.js')
.expect('Content-Type', 'application/javascript; charset=UTF-8')
// TODO(michael-ciniawsky) investigate the need for this test
.expect('Content-Length', '4631')
.expect(200, /console\.log\('Hey\.'\)/, done);
});

it('request to existing image, with transform rules on the /public/ URL', (done) => {
request(app).get('/public/svg.svg')
.expect('Content-Type', 'image/svg+xml; charset=UTF-8')
.expect('Content-Length', '4778')
.expect(200, done);
});

it('GET request to bundle file, with transform rules on the /public/ URL', (done) => {
request(app).get('/public/001/aliased-bundle.js')
.expect('Content-Type', 'application/javascript; charset=UTF-8')
.expect('Content-Length', '4631')
.expect(200, /console\.log\('Hey\.'\)/, done);
});

it('request to non existing file, with transform rules on the /public/ URL', (done) => {
request(app).get('/public/nope')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(404, done);
});

it('request to directory', (done) => {
request(app).get('/public/')
.expect('Content-Type', 'text/html; charset=UTF-8')
.expect('Content-Length', '16')
.expect(200, /Modified Index\./, done);
});
});

describe('no index mode', () => {
before((done) => {
app = express();
Expand Down