Ensure static image works correctly with basePath (#29307)

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: https://github.com/vercel/next.js/issues/29289
This commit is contained in:
JJ Kasper 2021-09-23 18:26:51 -05:00 committed by GitHub
parent 9343b67c11
commit 5dbb8704cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 191 additions and 3 deletions

View file

@ -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') {
@ -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)

View file

@ -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`

View file

@ -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)

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 B

View file

@ -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()
})