diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b6f279038aefb..38e67f260869a 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -111,36 +111,37 @@ stages:
- script: |
node run-tests.js -g 1/1 --timings --azure --type unit
displayName: 'Run tests'
-
- - job: test_chrome_integration
- pool:
- vmImage: 'windows-2019'
- strategy:
- matrix:
- nodejs-1:
- group: 1/4
- nodejs-2:
- group: 2/4
- nodejs-3:
- group: 3/4
- nodejs-4:
- group: 4/4
- steps:
- - checkout: none
- - script: |
- wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value
- displayName: 'List Chrome version'
- - task: NodeTool@0
- inputs:
- versionSpec: $(node_version)
- displayName: 'Install Node.js'
- - task: Cache@2
- inputs:
- # use deterministic cache key that is specific
- # to this test run
- key: $(Build.SourceVersion)
- path: $(System.DefaultWorkingDirectory)
- displayName: Cache Build
- - script: |
- node run-tests.js -g $(group) --timings --azure
- displayName: 'Run tests'
+ # TODO: investigate re-enabling when stability matches running in
+ # tests in ubuntu environment
+ # - job: test_chrome_integration
+ # pool:
+ # vmImage: 'windows-2019'
+ # strategy:
+ # matrix:
+ # nodejs-1:
+ # group: 1/4
+ # nodejs-2:
+ # group: 2/4
+ # nodejs-3:
+ # group: 3/4
+ # nodejs-4:
+ # group: 4/4
+ # steps:
+ # - checkout: none
+ # - script: |
+ # wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value
+ # displayName: 'List Chrome version'
+ # - task: NodeTool@0
+ # inputs:
+ # versionSpec: $(node_version)
+ # displayName: 'Install Node.js'
+ # - task: Cache@2
+ # inputs:
+ # # use deterministic cache key that is specific
+ # # to this test run
+ # key: $(Build.SourceVersion)
+ # path: $(System.DefaultWorkingDirectory)
+ # displayName: Cache Build
+ # - script: |
+ # node run-tests.js -g $(group) --timings --azure
+ # displayName: 'Run tests'
diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index cd186ab241574..099a4dc3f33bc 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -445,8 +445,9 @@ export default function Image({
...(placeholder === 'blur'
? {
filter: 'blur(20px)',
- backgroundSize: 'cover',
+ backgroundSize: objectFit || 'cover',
backgroundImage: `url("${blurDataURL}")`,
+ backgroundPosition: objectPosition || '0% 0%',
}
: undefined),
}
diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts
index 5def834f5993e..30a175203e7c2 100644
--- a/packages/next/server/image-optimizer.ts
+++ b/packages/next/server/image-optimizer.ts
@@ -22,7 +22,7 @@ const PNG = 'image/png'
const JPEG = 'image/jpeg'
const GIF = 'image/gif'
const SVG = 'image/svg+xml'
-const CACHE_VERSION = 2
+const CACHE_VERSION = 3
const MODERN_TYPES = [/* AVIF, */ WEBP]
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
const VECTOR_TYPES = [SVG]
@@ -35,7 +35,8 @@ export async function imageOptimizer(
res: ServerResponse,
parsedUrl: UrlWithParsedQuery,
nextConfig: NextConfig,
- distDir: string
+ distDir: string,
+ isDev = false
) {
const imageData: ImageConfig = nextConfig.images || imageConfigDefault
const { deviceSizes = [], imageSizes = [], domains = [], loader } = imageData
@@ -158,24 +159,24 @@ export async function imageOptimizer(
if (await fileExists(hashDir, 'directory')) {
const files = await promises.readdir(hashDir)
for (let file of files) {
- const [prefix, etag, extension] = file.split('.')
- const expireAt = Number(prefix)
+ const [maxAgeStr, expireAtSt, etag, extension] = file.split('.')
+ const maxAge = Number(maxAgeStr)
+ const expireAt = Number(expireAtSt)
const contentType = getContentType(extension)
const fsPath = join(hashDir, file)
if (now < expireAt) {
- res.setHeader(
- 'Cache-Control',
- isStatic
- ? 'public, max-age=315360000, immutable'
- : 'public, max-age=0, must-revalidate'
+ const result = setResponseHeaders(
+ req,
+ res,
+ etag,
+ maxAge,
+ contentType,
+ isStatic,
+ isDev
)
- if (sendEtagResponse(req, res, etag)) {
- return { finished: true }
+ if (!result.finished) {
+ createReadStream(fsPath).pipe(res)
}
- if (contentType) {
- res.setHeader('Content-Type', contentType)
- }
- createReadStream(fsPath).pipe(res)
return { finished: true }
} else {
await promises.unlink(fsPath)
@@ -271,8 +272,22 @@ export async function imageOptimizer(
const animate =
ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer)
if (vector || animate) {
- await writeToCacheDir(hashDir, upstreamType, expireAt, upstreamBuffer)
- sendResponse(req, res, upstreamType, upstreamBuffer, isStatic)
+ await writeToCacheDir(
+ hashDir,
+ upstreamType,
+ maxAge,
+ expireAt,
+ upstreamBuffer
+ )
+ sendResponse(
+ req,
+ res,
+ maxAge,
+ upstreamType,
+ upstreamBuffer,
+ isStatic,
+ isDev
+ )
return { finished: true }
}
@@ -342,13 +357,35 @@ export async function imageOptimizer(
}
if (optimizedBuffer) {
- await writeToCacheDir(hashDir, contentType, expireAt, optimizedBuffer)
- sendResponse(req, res, contentType, optimizedBuffer, isStatic)
+ await writeToCacheDir(
+ hashDir,
+ contentType,
+ maxAge,
+ expireAt,
+ optimizedBuffer
+ )
+ sendResponse(
+ req,
+ res,
+ maxAge,
+ contentType,
+ optimizedBuffer,
+ isStatic,
+ isDev
+ )
} else {
throw new Error('Unable to optimize buffer')
}
} catch (error) {
- sendResponse(req, res, upstreamType, upstreamBuffer, isStatic)
+ sendResponse(
+ req,
+ res,
+ maxAge,
+ upstreamType,
+ upstreamBuffer,
+ isStatic,
+ isDev
+ )
}
return { finished: true }
@@ -362,37 +399,64 @@ export async function imageOptimizer(
async function writeToCacheDir(
dir: string,
contentType: string,
+ maxAge: number,
expireAt: number,
buffer: Buffer
) {
await promises.mkdir(dir, { recursive: true })
const extension = getExtension(contentType)
const etag = getHash([buffer])
- const filename = join(dir, `${expireAt}.${etag}.${extension}`)
+ const filename = join(dir, `${maxAge}.${expireAt}.${etag}.${extension}`)
await promises.writeFile(filename, buffer)
}
-function sendResponse(
+function setResponseHeaders(
req: IncomingMessage,
res: ServerResponse,
+ etag: string,
+ maxAge: number,
contentType: string | null,
- buffer: Buffer,
- isStatic: boolean
+ isStatic: boolean,
+ isDev: boolean
) {
- const etag = getHash([buffer])
res.setHeader(
'Cache-Control',
isStatic
? 'public, max-age=315360000, immutable'
- : 'public, max-age=0, must-revalidate'
+ : `public, max-age=${isDev ? 0 : maxAge}, must-revalidate`
)
if (sendEtagResponse(req, res, etag)) {
- return
+ // already called res.end() so we're finished
+ return { finished: true }
}
if (contentType) {
res.setHeader('Content-Type', contentType)
}
- res.end(buffer)
+ return { finished: false }
+}
+
+function sendResponse(
+ req: IncomingMessage,
+ res: ServerResponse,
+ maxAge: number,
+ contentType: string | null,
+ buffer: Buffer,
+ isStatic: boolean,
+ isDev: boolean
+) {
+ const etag = getHash([buffer])
+ const result = setResponseHeaders(
+ req,
+ res,
+ etag,
+ maxAge,
+ contentType,
+ isStatic,
+ isDev
+ )
+ if (!result.finished) {
+ res.end(buffer)
+ }
}
function getSupportedMimeType(options: string[], accept = ''): string {
diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts
index 32ef98432e60c..4485694598587 100644
--- a/packages/next/server/next-server.ts
+++ b/packages/next/server/next-server.ts
@@ -789,7 +789,8 @@ export default class Server {
res,
parsedUrl,
server.nextConfig,
- server.distDir
+ server.distDir,
+ this.renderOpts.dev
),
},
{
diff --git a/test/integration/image-component/default/pages/blurry-placeholder.js b/test/integration/image-component/default/pages/blurry-placeholder.js
index 4ebc925cbe94c..58751c0b9afb3 100644
--- a/test/integration/image-component/default/pages/blurry-placeholder.js
+++ b/test/integration/image-component/default/pages/blurry-placeholder.js
@@ -15,6 +15,17 @@ export default function Page() {
blurDataURL="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E"
/>
+
+
{
return (
diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js
index 1f090f0e4aa40..b10ba3a0a7102 100644
--- a/test/integration/image-component/default/test/index.test.js
+++ b/test/integration/image-component/default/test/index.test.js
@@ -640,6 +640,14 @@ describe('Image Component Tests', () => {
`background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E")`
)
+ expect($html('#blurry-placeholder')[0].attribs.style).toContain(
+ `background-position:0% 0%`
+ )
+
+ expect(
+ $html('#blurry-placeholder-tall-centered')[0].attribs.style
+ ).toContain(`background-position:center`)
+
expect($html('#blurry-placeholder-with-lazy')[0].attribs.style).toContain(
`background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400' viewBox='0 0 400 400'%3E%3Cfilter id='blur' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='20' edgeMode='duplicate' /%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1 1' /%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Cimage filter='url(%23blur)' href='data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlT/wAALCAAKAAoBAREA/8QAMwABAQEAAAAAAAAAAAAAAAAAAAcJEAABAwUAAwAAAAAAAAAAAAAFAAYRAQMEEyEVMlH/2gAIAQEAAD8Az1bLPaxhiuk0QdeCOLDtHixN2dmd2bsc5FPX7VTREX//2Q==' x='0' y='0' height='100%25' width='100%25'/%3E%3C/svg%3E")`
)
diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js
index 0e431822186a2..666d80d31a703 100644
--- a/test/integration/image-component/default/test/static.test.js
+++ b/test/integration/image-component/default/test/static.test.js
@@ -38,12 +38,12 @@ const runTests = () => {
})
it('Should add a blurry placeholder to statically imported jpg', async () => {
expect(html).toContain(
- `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;filter:blur(20px);background-size:cover;background-image:url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/sBCgoKCgoKCwwMCw8QDhAPFhQTExQWIhgaGBoYIjMgJSAgJSAzLTcsKSw3LVFAODhAUV5PSk9ecWVlcY+Ij7u7+//CABEIAAgACAMBIgACEQEDEQH/xAAUAAEAAAAAAAAAAAAAAAAAAAAH/9oACAEBAAAAADX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oACAECEAAAAH//xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDEAAAAH//xAAdEAABAgcAAAAAAAAAAAAAAAATEhUAAwUUIzLS/9oACAEBAAE/AB0ZlUac43GqMYuo/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k=")"`
+ `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;filter:blur(20px);background-size:cover;background-image:url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/sBCgoKCgoKCwwMCw8QDhAPFhQTExQWIhgaGBoYIjMgJSAgJSAzLTcsKSw3LVFAODhAUV5PSk9ecWVlcY+Ij7u7+//CABEIAAgACAMBIgACEQEDEQH/xAAUAAEAAAAAAAAAAAAAAAAAAAAH/9oACAEBAAAAADX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oACAECEAAAAH//xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDEAAAAH//xAAdEAABAgcAAAAAAAAAAAAAAAATEhUAAwUUIzLS/9oACAEBAAE/AB0ZlUac43GqMYuo/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k=");background-position:0% 0%"`
)
})
it('Should add a blurry placeholder to statically imported png', async () => {
expect(html).toContain(
- `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;filter:blur(20px);background-size:cover;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAQAAABuBnYAAAAATklEQVR42i2I0QmAMBQD869Q9K+IsxU6RkfoiA6T55VXDpJLJC9uUJIzcx+XFd2dXMbx8n+QpoeYDpgY66RaDA83jCUfVpK2pER1dcEUP+KfSBtXK+BpAAAAAElFTkSuQmCC")"`
+ `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;filter:blur(20px);background-size:cover;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAQAAABuBnYAAAAATklEQVR42i2I0QmAMBQD869Q9K+IsxU6RkfoiA6T55VXDpJLJC9uUJIzcx+XFd2dXMbx8n+QpoeYDpgY66RaDA83jCUfVpK2pER1dcEUP+KfSBtXK+BpAAAAAElFTkSuQmCC");background-position:0% 0%"`
)
})
}
diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js
index e553e0a31dc19..161301d3d28b3 100644
--- a/test/integration/image-optimizer/test/index.test.js
+++ b/test/integration/image-optimizer/test/index.test.js
@@ -13,7 +13,6 @@ import {
} from 'next-test-utils'
import isAnimated from 'next/dist/compiled/is-animated'
import { join } from 'path'
-import { createHash } from 'crypto'
jest.setTimeout(1000 * 60 * 2)
@@ -56,8 +55,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('image/gif')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
expect(isAnimated(await res.buffer())).toBe(true)
@@ -68,8 +67,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('image/png')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
expect(isAnimated(await res.buffer())).toBe(true)
@@ -80,8 +79,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('image/webp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
expect(isAnimated(await res.buffer())).toBe(true)
@@ -93,8 +92,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toContain('image/svg+xml')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
const actual = await res.text()
@@ -111,8 +110,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toContain('image/x-icon')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
const actual = await res.text()
@@ -131,8 +130,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toContain('image/jpeg')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
})
@@ -145,8 +144,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toContain('image/png')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
})
@@ -242,8 +241,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
@@ -255,8 +254,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/png')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
@@ -268,8 +267,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/png')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
@@ -281,8 +280,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/gif')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
// FIXME: await expectWidth(res, w)
@@ -294,8 +293,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/tiff')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
// FIXME: await expectWidth(res, w)
@@ -309,8 +308,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
@@ -324,8 +323,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
@@ -344,8 +343,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
@@ -447,7 +446,7 @@ function runTests({ w, isDev, domains }) {
expect(res1.status).toBe(200)
expect(res1.headers.get('Content-Type')).toBe('image/webp')
expect(res1.headers.get('Cache-Control')).toBe(
- 'public, max-age=0, must-revalidate'
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
const etag = res1.headers.get('Etag')
expect(etag).toBeTruthy()
@@ -458,6 +457,9 @@ function runTests({ w, isDev, domains }) {
expect(res2.status).toBe(304)
expect(res2.headers.get('Content-Type')).toBeFalsy()
expect(res2.headers.get('Etag')).toBe(etag)
+ expect(res2.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
+ )
expect((await res2.buffer()).length).toBe(0)
const query3 = { url: '/test.jpg', w, q: 25 }
@@ -465,7 +467,7 @@ function runTests({ w, isDev, domains }) {
expect(res3.status).toBe(200)
expect(res3.headers.get('Content-Type')).toBe('image/webp')
expect(res3.headers.get('Cache-Control')).toBe(
- 'public, max-age=0, must-revalidate'
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res3.headers.get('Etag')).toBeTruthy()
expect(res3.headers.get('Etag')).not.toBe(etag)
@@ -481,8 +483,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/bmp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
@@ -496,8 +498,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, 400)
@@ -511,8 +513,8 @@ function runTests({ w, isDev, domains }) {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/png')
- expect(res.headers.get('cache-control')).toBe(
- 'public, max-age=0, must-revalidate'
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
const png = await res.buffer()
@@ -535,7 +537,7 @@ function runTests({ w, isDev, domains }) {
const res1 = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res1.status).toBe(200)
- expect(res1.headers.get('cache-control')).toBe(
+ expect(res1.headers.get('Cache-Control')).toBe(
'public, max-age=315360000, immutable'
)
await expectWidth(res1, w)
@@ -543,7 +545,7 @@ function runTests({ w, isDev, domains }) {
// Ensure subsequent request also has immutable header
const res2 = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res2.status).toBe(200)
- expect(res2.headers.get('cache-control')).toBe(
+ expect(res2.headers.get('Cache-Control')).toBe(
'public, max-age=315360000, immutable'
)
await expectWidth(res2, w)
@@ -559,6 +561,7 @@ function runTests({ w, isDev, domains }) {
})
it('should handle concurrent requests', async () => {
+ await fs.remove(imagesDir)
const query = { url: '/test.png', w, q: 80 }
const opts = { headers: { accept: 'image/webp,*/*' } }
const [res1, res2] = await Promise.all([
@@ -572,19 +575,8 @@ function runTests({ w, isDev, domains }) {
await expectWidth(res1, w)
await expectWidth(res2, w)
- // There should be only one image created in the cache directory.
- const hashItems = [2, '/test.png', w, 80, 'image/webp']
- const hash = createHash('sha256')
- for (let item of hashItems) {
- if (typeof item === 'number') hash.update(String(item))
- else {
- hash.update(item)
- }
- }
- const hashDir = hash.digest('base64').replace(/\//g, '-')
- const dir = join(imagesDir, hashDir)
- const files = await fs.readdir(dir)
- expect(files.length).toBe(1)
+ const json1 = await fsToJson(imagesDir)
+ expect(Object.keys(json1).length).toBe(1)
})
}
@@ -720,6 +712,7 @@ describe('Image Optimizer', () => {
const domains = [
'localhost',
'example.com',
+ 'assets.vercel.com',
'image-optimization-test.vercel.app',
]
@@ -861,10 +854,10 @@ describe('Image Optimizer', () => {
},
}`
nextConfig.replace('{ /* replaceme */ }', newConfig)
+ await nextBuild(appDir)
appPort = await findPort()
- app = await launchApp(appDir, appPort)
+ app = await nextStart(appDir, appPort)
})
-
afterAll(async () => {
await killApp(app)
nextConfig.restore()
@@ -877,6 +870,9 @@ describe('Image Optimizer', () => {
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('image/webp')
+ expect(res.headers.get('Cache-Control')).toBe(
+ `public, max-age=31536000, must-revalidate`
+ )
await expectWidth(res, 64)
})
})
diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js
index 20bd919c4ca0d..591cba62d5b90 100644
--- a/test/integration/production/test/index.test.js
+++ b/test/integration/production/test/index.test.js
@@ -917,26 +917,30 @@ describe('Production Usage', () => {
expect(missing).toBe(false)
})
- it('should preserve query when hard navigating from page 404', async () => {
- const browser = await webdriver(appPort, '/')
- await browser.eval(`(function() {
- window.beforeNav = 1
- window.next.router.push({
- pathname: '/non-existent',
- query: { hello: 'world' }
- })
- })()`)
+ if (global.browserName !== 'internet explorer') {
+ it('should preserve query when hard navigating from page 404', async () => {
+ const browser = await webdriver(appPort, '/')
+ await browser.eval(`(function() {
+ window.beforeNav = 1
+ window.next.router.push({
+ pathname: '/non-existent',
+ query: { hello: 'world' }
+ })
+ })()`)
- await check(
- () => browser.eval('document.documentElement.innerHTML'),
- /page could not be found/
- )
+ await check(
+ () => browser.eval('document.documentElement.innerHTML'),
+ /page could not be found/
+ )
- expect(await browser.eval('window.beforeNav')).toBe(null)
- expect(await browser.eval('window.location.hash')).toBe('')
- expect(await browser.eval('window.location.search')).toBe('?hello=world')
- expect(await browser.eval('window.location.pathname')).toBe('/non-existent')
- })
+ expect(await browser.eval('window.beforeNav')).toBe(null)
+ expect(await browser.eval('window.location.hash')).toBe('')
+ expect(await browser.eval('window.location.search')).toBe('?hello=world')
+ expect(await browser.eval('window.location.pathname')).toBe(
+ '/non-existent'
+ )
+ })
+ }
if (!process.env.NEXT_PRIVATE_TEST_WEBPACK4_MODE) {
it('should remove placeholder for next/image correctly', async () => {