-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[optimize] inject publicPath at request time (#14007)
* [optimize] inject publicPath at request time * [optimize/getFileHash] finish doc block * [optimize/bundlesRoute] correct return value doc type * [optimize/bundleRoute] use more descriptive name for file hash cache * [optimize/dynamicAssetResponse] add more details to doc * [utils/createReplaceStream] trim the buffer based on the length of toReplace, not replacement * [utils/createReplaceStream] add inline docs * [utils/createReplaceStream] write unit tests * [optimize/bundleRoute] expect supports buffers * [optimize/bundleRoute/basePublicPath/tests] add happy path * [optimize/bundlesRoute/tests] verify content-type header * [optimize/bundlesRoute] use ' (cherry picked from commit 1ea82fa)
- Loading branch information
Showing
23 changed files
with
765 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
import { resolve } from 'path'; | ||
import { readFileSync } from 'fs'; | ||
import crypto from 'crypto'; | ||
|
||
import Chance from 'chance'; | ||
import expect from 'expect.js'; | ||
import Hapi from 'hapi'; | ||
import Inert from 'inert'; | ||
import sinon from 'sinon'; | ||
|
||
import { createBundlesRoute } from '../bundles_route'; | ||
import { PUBLIC_PATH_PLACEHOLDER } from '../../public_path_placeholder'; | ||
|
||
const chance = new Chance(); | ||
const outputFixture = resolve(__dirname, './fixtures/output'); | ||
|
||
function replaceAll(source, replace, replaceWith) { | ||
return source.split(replace).join(replaceWith); | ||
} | ||
|
||
describe('optimizer/bundle route', () => { | ||
const sandbox = sinon.sandbox.create(); | ||
|
||
function createServer(options = {}) { | ||
const { | ||
bundlesPath = outputFixture, | ||
basePublicPath = '' | ||
} = options; | ||
|
||
const server = new Hapi.Server(); | ||
server.connection({ port: 0 }); | ||
server.register([Inert]); | ||
|
||
server.route(createBundlesRoute({ | ||
bundlesPath, | ||
basePublicPath, | ||
})); | ||
|
||
return server; | ||
} | ||
|
||
afterEach(() => sandbox.restore()); | ||
|
||
describe('validation', () => { | ||
it('validates that bundlesPath is an absolute path', () => { | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: null, | ||
basePublicPath: '' | ||
}); | ||
}).to.throwError(/absolute path/); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: './relative', | ||
basePublicPath: '' | ||
}); | ||
}).to.throwError(/absolute path/); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: 1234, | ||
basePublicPath: '' | ||
}); | ||
}).to.throwError(/absolute path/); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/absolute/path', | ||
basePublicPath: '' | ||
}); | ||
}).to.not.throwError(); | ||
}); | ||
it('validates that basePublicPath is valid', () => { | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/bundles', | ||
basePublicPath: 123 | ||
}); | ||
}).to.throwError(/string/); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/bundles', | ||
basePublicPath: {} | ||
}); | ||
}).to.throwError(/string/); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/bundles', | ||
basePublicPath: '/a/' | ||
}); | ||
}).to.throwError(/start and not end with a \//); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/bundles', | ||
basePublicPath: 'a/' | ||
}); | ||
}).to.throwError(/start and not end with a \//); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/bundles', | ||
basePublicPath: '/a' | ||
}); | ||
}).to.not.throwError(); | ||
expect(() => { | ||
createBundlesRoute({ | ||
bundlesPath: '/bundles', | ||
basePublicPath: '' | ||
}); | ||
}).to.not.throwError(); | ||
}); | ||
}); | ||
|
||
describe('image', () => { | ||
it('responds with exact file data', async () => { | ||
const server = createServer(); | ||
const response = await server.inject({ | ||
url: '/bundles/image.png' | ||
}); | ||
|
||
expect(response.statusCode).to.be(200); | ||
const image = readFileSync(resolve(outputFixture, 'image.png')); | ||
expect(response.headers).to.have.property('content-length', image.length); | ||
expect(response.headers).to.have.property('content-type', 'image/png'); | ||
expect(image).to.eql(response.rawPayload); | ||
}); | ||
}); | ||
|
||
describe('js file without placeholder', () => { | ||
it('responds with no content-length and exact file data', async () => { | ||
const server = createServer(); | ||
const response = await server.inject({ | ||
url: '/bundles/no_placeholder.js' | ||
}); | ||
|
||
expect(response.statusCode).to.be(200); | ||
expect(response.headers).to.not.have.property('content-length'); | ||
expect(response.headers).to.have.property('content-type', 'application/javascript; charset=utf-8'); | ||
expect(readFileSync(resolve(outputFixture, 'no_placeholder.js'))) | ||
.to.eql(response.rawPayload); | ||
}); | ||
}); | ||
|
||
describe('js file with placeholder', () => { | ||
it('responds with no content-length and modified file data', async () => { | ||
const basePublicPath = `/${chance.word()}`; | ||
const server = createServer({ basePublicPath }); | ||
|
||
const response = await server.inject({ | ||
url: '/bundles/with_placeholder.js' | ||
}); | ||
|
||
expect(response.statusCode).to.be(200); | ||
const source = readFileSync(resolve(outputFixture, 'with_placeholder.js'), 'utf8'); | ||
expect(response.headers).to.not.have.property('content-length'); | ||
expect(response.headers).to.have.property('content-type', 'application/javascript; charset=utf-8'); | ||
expect(response.result.indexOf(source)).to.be(-1); | ||
expect(response.result).to.be(replaceAll( | ||
source, | ||
PUBLIC_PATH_PLACEHOLDER, | ||
`${basePublicPath}/bundles/` | ||
)); | ||
}); | ||
}); | ||
|
||
describe('css file without placeholder', () => { | ||
it('responds with no content-length and exact file data', async () => { | ||
const server = createServer(); | ||
const response = await server.inject({ | ||
url: '/bundles/no_placeholder.css' | ||
}); | ||
|
||
expect(response.statusCode).to.be(200); | ||
expect(response.headers).to.not.have.property('content-length'); | ||
expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8'); | ||
expect(readFileSync(resolve(outputFixture, 'no_placeholder.css'))) | ||
.to.eql(response.rawPayload); | ||
}); | ||
}); | ||
|
||
describe('css file with placeholder', () => { | ||
it('responds with no content-length and modified file data', async () => { | ||
const basePublicPath = `/${chance.word()}`; | ||
const server = createServer({ basePublicPath }); | ||
|
||
const response = await server.inject({ | ||
url: '/bundles/with_placeholder.css' | ||
}); | ||
|
||
expect(response.statusCode).to.be(200); | ||
const source = readFileSync(resolve(outputFixture, 'with_placeholder.css'), 'utf8'); | ||
expect(response.headers).to.not.have.property('content-length'); | ||
expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8'); | ||
expect(response.result.indexOf(source)).to.be(-1); | ||
expect(response.result).to.be(replaceAll( | ||
source, | ||
PUBLIC_PATH_PLACEHOLDER, | ||
`${basePublicPath}/bundles/` | ||
)); | ||
}); | ||
}); | ||
|
||
describe('js file outside bundlesPath', () => { | ||
it('responds with a 403', async () => { | ||
const server = createServer(); | ||
|
||
const response = await server.inject({ | ||
url: '/bundles/../outside_output.js' | ||
}); | ||
|
||
expect(response.statusCode).to.be(403); | ||
expect(response.result).to.eql({ | ||
error: 'Forbidden', | ||
message: 'Forbidden', | ||
statusCode: 403 | ||
}); | ||
}); | ||
}); | ||
|
||
describe('missing js file', () => { | ||
it('responds with 404', async () => { | ||
const server = createServer(); | ||
|
||
const response = await server.inject({ | ||
url: '/bundles/non_existant.js' | ||
}); | ||
|
||
expect(response.statusCode).to.be(404); | ||
expect(response.result).to.eql({ | ||
error: 'Not Found', | ||
message: 'Not Found', | ||
statusCode: 404 | ||
}); | ||
}); | ||
}); | ||
|
||
describe('missing bundlesPath', () => { | ||
it('responds with 404', async () => { | ||
const server = createServer({ | ||
bundlesPath: resolve(__dirname, 'fixtures/not_really_output') | ||
}); | ||
|
||
const response = await server.inject({ | ||
url: '/bundles/with_placeholder.js' | ||
}); | ||
|
||
expect(response.statusCode).to.be(404); | ||
expect(response.result).to.eql({ | ||
error: 'Not Found', | ||
message: 'Not Found', | ||
statusCode: 404 | ||
}); | ||
}); | ||
}); | ||
|
||
describe('etag', () => { | ||
it('only calculates hash of file on first request', async () => { | ||
const createHash = sandbox.spy(crypto, 'createHash'); | ||
|
||
const server = createServer(); | ||
|
||
sinon.assert.notCalled(createHash); | ||
const resp1 = await server.inject({ | ||
url: '/bundles/no_placeholder.js' | ||
}); | ||
|
||
sinon.assert.calledOnce(createHash); | ||
createHash.reset(); | ||
expect(resp1.statusCode).to.be(200); | ||
|
||
const resp2 = await server.inject({ | ||
url: '/bundles/no_placeholder.js' | ||
}); | ||
|
||
sinon.assert.notCalled(createHash); | ||
expect(resp2.statusCode).to.be(200); | ||
}); | ||
|
||
it('is unique per basePublicPath although content is the same', async () => { | ||
const basePublicPath1 = `/${chance.word()}`; | ||
const basePublicPath2 = `/${chance.word()}`; | ||
|
||
const [resp1, resp2] = await Promise.all([ | ||
createServer({ basePublicPath: basePublicPath1 }).inject({ | ||
url: '/bundles/no_placeholder.js' | ||
}), | ||
createServer({ basePublicPath: basePublicPath2 }).inject({ | ||
url: '/bundles/no_placeholder.js' | ||
}), | ||
]); | ||
|
||
expect(resp1.statusCode).to.be(200); | ||
expect(resp2.statusCode).to.be(200); | ||
|
||
expect(resp1.rawPayload).to.eql(resp2.rawPayload); | ||
|
||
expect(resp1.headers.etag).to.be.a('string'); | ||
expect(resp2.headers.etag).to.be.a('string'); | ||
expect(resp1.headers.etag).to.not.eql(resp2.headers.etag); | ||
}); | ||
}); | ||
|
||
describe('cache control', () => { | ||
it('responds with 304 when etag and last modified are sent back', async () => { | ||
const server = createServer(); | ||
const resp = await server.inject({ | ||
url: '/bundles/with_placeholder.js' | ||
}); | ||
|
||
expect(resp.statusCode).to.be(200); | ||
|
||
const resp2 = await server.inject({ | ||
url: '/bundles/with_placeholder.js', | ||
headers: { | ||
'if-modified-since': resp.headers['last-modified'], | ||
'if-none-match': resp.headers.etag | ||
} | ||
}); | ||
|
||
expect(resp2.statusCode).to.be(304); | ||
expect(resp2.result).to.have.length(0); | ||
}); | ||
}); | ||
}); |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions
3
src/optimize/bundles_route/__tests__/fixtures/output/no_placeholder.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
body { | ||
background-color: goldenrod; | ||
} |
1 change: 1 addition & 0 deletions
1
src/optimize/bundles_route/__tests__/fixtures/output/no_placeholder.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
console.log('chunk2'); |
3 changes: 3 additions & 0 deletions
3
src/optimize/bundles_route/__tests__/fixtures/output/with_placeholder.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
body { | ||
background-image: url(__REPLACE_WITH_PUBLIC_PATH__/image.png); | ||
} |
1 change: 1 addition & 0 deletions
1
src/optimize/bundles_route/__tests__/fixtures/output/with_placeholder.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
console.log('__REPLACE_WITH_PUBLIC_PATH__'); |
1 change: 1 addition & 0 deletions
1
src/optimize/bundles_route/__tests__/fixtures/outside_output.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
console.log('outside output'); |
Oops, something went wrong.