diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index e4aff1b76e32f..4a4198b39154e 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -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) diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts index 5690e59b984d7..24fa19b07bdab 100644 --- a/packages/next/server/config.ts +++ b/packages/next/server/config.ts @@ -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` diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index a2d7c6a664d0c..4a4227aeeb1f5 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -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) diff --git a/test/integration/image-component/base-path/components/TallImage.js b/test/integration/image-component/base-path/components/TallImage.js new file mode 100644 index 0000000000000..c0fbbcfe6d63c --- /dev/null +++ b/test/integration/image-component/base-path/components/TallImage.js @@ -0,0 +1,20 @@ +import React from 'react' +import Image from 'next/image' + +import testTall from './tall.png' + +const Page = () => { + return ( +
+

Static Image

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-component/base-path/components/tall.png b/test/integration/image-component/base-path/components/tall.png new file mode 100644 index 0000000000000..a792dda6c172f Binary files /dev/null and b/test/integration/image-component/base-path/components/tall.png differ diff --git a/test/integration/image-component/base-path/pages/static-img.js b/test/integration/image-component/base-path/pages/static-img.js new file mode 100644 index 0000000000000..29912c6defc24 --- /dev/null +++ b/test/integration/image-component/base-path/pages/static-img.js @@ -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 ( +
+

Static Image

+ + + + + +
+ + + + + + + +
+ +
+ ) +} + +export default Page diff --git a/test/integration/image-component/base-path/public/foo/test-rect.jpg b/test/integration/image-component/base-path/public/foo/test-rect.jpg new file mode 100644 index 0000000000000..68d3a8415f5e6 Binary files /dev/null and b/test/integration/image-component/base-path/public/foo/test-rect.jpg differ diff --git a/test/integration/image-component/base-path/public/test.ico b/test/integration/image-component/base-path/public/test.ico new file mode 100644 index 0000000000000..55cce0b4a8547 Binary files /dev/null and b/test/integration/image-component/base-path/public/test.ico differ diff --git a/test/integration/image-component/base-path/public/test.webp b/test/integration/image-component/base-path/public/test.webp new file mode 100644 index 0000000000000..4b306cb0898cc Binary files /dev/null and b/test/integration/image-component/base-path/public/test.webp differ diff --git a/test/integration/image-component/base-path/test/static.test.js b/test/integration/image-component/base-path/test/static.test.js new file mode 100644 index 0000000000000..c3b050cb54387 --- /dev/null +++ b/test/integration/image-component/base-path/test/static.test.js @@ -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("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");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() +})