Skip to content

Commit

Permalink
Ensure static image works correctly with basePath (#29307)
Browse files Browse the repository at this point in the history
This ensures we prefix the `src` for static images with the `basePath` correctly, this also copies over the static image tests to the basePath image-component suite. 

## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Errors have helpful link attached, see `contributing.md`

Fixes: #29289
  • Loading branch information
ijjk authored Sep 23, 2021
1 parent 9343b67 commit 5dbb870
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 3 deletions.
4 changes: 2 additions & 2 deletions packages/next/build/webpack/loaders/next-image-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function nextImageLoader(content) {
'/static/image/[path][name].[hash].[ext]',
opts
)
const outputPath = '/_next' + interpolatedName
const outputPath = assetPrefix + '/_next' + interpolatedName

let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
Expand All @@ -32,7 +32,7 @@ function nextImageLoader(content) {
if (isDev) {
const prefix = 'http://localhost'
const url = new URL('/_next/image', prefix)
url.searchParams.set('url', assetPrefix + outputPath)
url.searchParams.set('url', outputPath)
url.searchParams.set('w', BLUR_IMG_SIZE)
url.searchParams.set('q', BLUR_QUALITY)
blurDataURL = url.href.slice(prefix.length)
Expand Down
7 changes: 7 additions & 0 deletions packages/next/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)
}

// static images are automatically prefixed with assetPrefix
// so we need to ensure _next/image allows downloading from
// this resource
if (config.assetPrefix?.startsWith('http')) {
images.domains.push(new URL(config.assetPrefix).hostname)
}

if (images.domains.length > 50) {
throw new Error(
`Specified images.domains exceeds length of 50, received length (${images.domains.length}), please reduce the length of the array to continue.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
Expand Down
4 changes: 3 additions & 1 deletion packages/next/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ export async function imageOptimizer(
}

// Should match output from next-image-loader
const isStatic = url.startsWith('/_next/static/image')
const isStatic = url.startsWith(
`${nextConfig.basePath || ''}/_next/static/image`
)

const width = parseInt(w, 10)

Expand Down
20 changes: 20 additions & 0 deletions test/integration/image-component/base-path/components/TallImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'
import Image from 'next/image'

import testTall from './tall.png'

const Page = () => {
return (
<div>
<h1 id="page-header">Static Image</h1>
<Image
id="basic-static"
src={testTall}
layout="fixed"
placeholder="blur"
/>
</div>
)
}

export default Page
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions test/integration/image-component/base-path/pages/static-img.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import testImg from '../public/foo/test-rect.jpg'
import Image from 'next/image'

import testJPG from '../public/test.jpg'
import testPNG from '../public/test.png'
import testWEBP from '../public/test.webp'
import testSVG from '../public/test.svg'
import testGIF from '../public/test.gif'
import testBMP from '../public/test.bmp'
import testICO from '../public/test.ico'

import TallImage from '../components/TallImage'

const Page = () => {
return (
<div>
<h1 id="page-header">Static Image</h1>
<Image
id="basic-static"
src={testImg}
layout="fixed"
placeholder="blur"
/>
<TallImage />
<Image
id="defined-size-static"
src={testPNG}
layout="fixed"
height="200"
width="200"
/>
<Image id="require-static" src={require('../public/foo/test-rect.jpg')} />
<Image
id="basic-non-static"
src="/test-rect.jpg"
width="400"
height="300"
/>
<br />
<Image id="blur-png" src={testPNG} placeholder="blur" />
<Image id="blur-jpg" src={testJPG} placeholder="blur" />
<Image id="blur-webp" src={testWEBP} placeholder="blur" />
<Image id="static-svg" src={testSVG} />
<Image id="static-gif" src={testGIF} />
<Image id="static-bmp" src={testBMP} />
<Image id="static-ico" src={testICO} />
<br />
<Image id="static-unoptimized" src={testJPG} unoptimized />
</div>
)
}

export default Page
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
105 changes: 105 additions & 0 deletions test/integration/image-component/base-path/test/static.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
findPort,
killApp,
nextBuild,
nextStart,
renderViaHTTP,
File,
waitFor,
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import { join } from 'path'

const appDir = join(__dirname, '../')
let appPort
let app
let browser
let html

const indexPage = new File(join(appDir, 'pages/static-img.js'))

const runTests = () => {
it('Should allow an image with a static src to omit height and width', async () => {
expect(await browser.elementById('basic-static')).toBeTruthy()
expect(await browser.elementById('blur-png')).toBeTruthy()
expect(await browser.elementById('blur-webp')).toBeTruthy()
expect(await browser.elementById('blur-jpg')).toBeTruthy()
expect(await browser.elementById('static-svg')).toBeTruthy()
expect(await browser.elementById('static-gif')).toBeTruthy()
expect(await browser.elementById('static-bmp')).toBeTruthy()
expect(await browser.elementById('static-ico')).toBeTruthy()
expect(await browser.elementById('static-unoptimized')).toBeTruthy()
})
it('Should use immutable cache-control header for static import', async () => {
await browser.eval(
`document.getElementById("basic-static").scrollIntoView()`
)
await waitFor(1000)
const url = await browser.eval(
`document.getElementById("basic-static").src`
)
const res = await fetch(url)
expect(res.headers.get('cache-control')).toBe(
'public, max-age=315360000, immutable'
)
})
it('Should use immutable cache-control header even when unoptimized', async () => {
await browser.eval(
`document.getElementById("static-unoptimized").scrollIntoView()`
)
await waitFor(1000)
const url = await browser.eval(
`document.getElementById("static-unoptimized").src`
)
const res = await fetch(url)
expect(res.headers.get('cache-control')).toBe(
'public, max-age=31536000, immutable'
)
})
it('Should automatically provide an image height and width', async () => {
expect(html).toContain('width:400px;height:300px')
})
it('Should allow provided width and height to override intrinsic', async () => {
expect(html).toContain('width:200px;height:200px')
expect(html).not.toContain('width:400px;height:400px')
})
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(&quot;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=&quot;);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(&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAQAAABuBnYAAAAATklEQVR42i2I0QmAMBQD869Q9K+IsxU6RkfoiA6T55VXDpJLJC9uUJIzcx+XFd2dXMbx8n+QpoeYDpgY66RaDA83jCUfVpK2pER1dcEUP+KfSBtXK+BpAAAAAElFTkSuQmCC&quot;);background-position:0% 0%"`
)
})
}

describe('Build Error Tests for basePath', () => {
it('should throw build error when import statement is used with missing file', async () => {
await indexPage.replace(
'../public/foo/test-rect.jpg',
'../public/foo/test-rect-broken.jpg'
)

const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
await indexPage.restore()

expect(stderr).toContain(
"Error: Can't resolve '../public/foo/test-rect-broken.jpg"
)
})
})
describe('Static Image Component Tests for basePath', () => {
beforeAll(async () => {
await nextBuild(appDir)
appPort = await findPort()
app = await nextStart(appDir, appPort)
html = await renderViaHTTP(appPort, '/docs/static-img')
browser = await webdriver(appPort, '/docs/static-img')
})
afterAll(() => {
killApp(app)
})
runTests()
})

0 comments on commit 5dbb870

Please sign in to comment.