From 996dc7f875d341d4b0d0d348a786adc587a39aa2 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 10 May 2021 10:05:49 -0700 Subject: [PATCH 01/34] Add static image loading functionality with auto height and width --- packages/next/build/webpack-config.ts | 5 +++ .../webpack/loaders/next-image-loader.js | 32 ++++++++++++++++++ packages/next/client/image.tsx | 22 ++++++++++-- packages/next/package.json | 1 + .../image-component/static/pages/index.js | 14 ++++++++ .../static/public/foo/test.jpg | Bin 0 -> 6765 bytes .../image-component/static/test/index.test.js | 27 +++++++++++++++ 7 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 packages/next/build/webpack/loaders/next-image-loader.js create mode 100644 test/integration/image-component/static/pages/index.js create mode 100644 test/integration/image-component/static/public/foo/test.jpg create mode 100644 test/integration/image-component/static/test/index.test.js diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index b19dbf1135219..6b52316db94fd 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -938,6 +938,7 @@ export default async function getBaseWebpackConfig( 'error-loader', 'next-babel-loader', 'next-client-pages-loader', + 'next-image-loader', 'next-serverless-loader', 'noop-loader', 'next-style-loader', @@ -986,6 +987,10 @@ export default async function getBaseWebpackConfig( ] : defaultLoaders.babel, }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + loader: 'next-image-loader', + }, ].filter(Boolean), }, plugins: [ diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js new file mode 100644 index 0000000000000..d9ce3115db41d --- /dev/null +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -0,0 +1,32 @@ +import loaderUtils from 'next/dist/compiled/loader-utils' +import sizeOf from 'image-size' + +function nextImageLoader(content) { + this.cacheable && this.cacheable(true) + this.addDependency(this.resourcePath) + + const query = loaderUtils.getOptions(this) + const context = query.context || this.rootContext || this.options.context + const regExp = query.regExp + const opts = { context, content, regExp } + const interpolatedName = loaderUtils.interpolateName( + this, + '/[path][name].[ext]', + opts + ) + //TODO: TEST FOR URL WITHOUT /public + const src = interpolatedName.slice(7) + const imageSize = sizeOf(this.resourcePath) + const esModule = typeof query.esModule !== 'undefined' ? query.esModule : true + const stringifiedData = JSON.stringify({ + src, + height: imageSize.height, + width: imageSize.width, + }) + + return `${ + esModule ? 'export default' : 'module.exports =' + } ${stringifiedData};` +} + +export default nextImageLoader diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index eea353be48980..b30115998dc84 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -49,11 +49,17 @@ type PlaceholderValue = 'blur' | 'empty' type ImgElementStyle = NonNullable +interface StaticImageData { + src: string + height: number + width: number +} + export type ImageProps = Omit< JSX.IntrinsicElements['img'], 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' > & { - src: string + src: string | StaticImageData loader?: ImageLoader quality?: number | string priority?: boolean @@ -273,9 +279,22 @@ export default function Image({ delete rest['layout'] } +<<<<<<< HEAD if (!configEnableBlurryPlaceholder) { placeholder = 'empty' } +======= + const isStatic = typeof src === 'object' + if (isStatic) { + const staticData = src as StaticImageData + if (!layout || layout !== 'fill') { + height = staticData.height + width = staticData.width + } + } + //static case is resolved, src is defintitely a string from here on. + src = (isStatic ? (src as StaticImageData).src : src) as string +>>>>>>> f9bd659e4 (Add static image loading functionality with auto height and width) if (process.env.NODE_ENV !== 'production') { if (!src) { @@ -310,7 +329,6 @@ export default function Image({ ) } } - let isLazy = !priority && (loading === 'lazy' || typeof loading === 'undefined') if (src && src.startsWith('data:')) { diff --git a/packages/next/package.json b/packages/next/package.json index 733c2c1ff0d61..61ed7dccd5f26 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -102,6 +102,7 @@ "raw-body": "2.4.1", "react-is": "16.13.1", "react-refresh": "0.8.3", + "image-size": "1.0.0", "stream-browserify": "3.0.0", "stream-http": "3.1.1", "string_decoder": "1.3.0", diff --git a/test/integration/image-component/static/pages/index.js b/test/integration/image-component/static/pages/index.js new file mode 100644 index 0000000000000..9944d227ae8ac --- /dev/null +++ b/test/integration/image-component/static/pages/index.js @@ -0,0 +1,14 @@ +import React from 'react' +import testImg from '../public/foo/test.jpg' +import Image from 'next/image' + +const Page = () => { + return ( +
+

Static Image

+ +
+ ) +} + +export default Page diff --git a/test/integration/image-component/static/public/foo/test.jpg b/test/integration/image-component/static/public/foo/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d536c882412ed3df0dc162823ca5146bcc033499 GIT binary patch literal 6765 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 { + it('Should allow an image with a static src to omit height and width', async () => { + expect(await browser.elementById('basic-static')).toBeTruthy() + }) +} + +describe('Static Image Component Tests', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + browser = await webdriver(appPort, '/') + }) + afterAll(() => killApp(app)) + runTests() +}) From 6dea6a24033a73fa9450e251819526a3e1e43c78 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 10 May 2021 15:55:57 -0700 Subject: [PATCH 02/34] Add tests for static image fill layout --- .../image-component/static/pages/index.js | 5 ++++- .../public/foo/{test.jpg => test-rect.jpg} | Bin 6765 -> 6089 bytes .../image-component/static/test/index.test.js | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) rename test/integration/image-component/static/public/foo/{test.jpg => test-rect.jpg} (56%) diff --git a/test/integration/image-component/static/pages/index.js b/test/integration/image-component/static/pages/index.js index 9944d227ae8ac..902b4121916d7 100644 --- a/test/integration/image-component/static/pages/index.js +++ b/test/integration/image-component/static/pages/index.js @@ -1,5 +1,5 @@ import React from 'react' -import testImg from '../public/foo/test.jpg' +import testImg from '../public/foo/test-rect.jpg' import Image from 'next/image' const Page = () => { @@ -7,6 +7,9 @@ const Page = () => {

Static Image

+
+ +
) } diff --git a/test/integration/image-component/static/public/foo/test.jpg b/test/integration/image-component/static/public/foo/test-rect.jpg similarity index 56% rename from test/integration/image-component/static/public/foo/test.jpg rename to test/integration/image-component/static/public/foo/test-rect.jpg index d536c882412ed3df0dc162823ca5146bcc033499..68d3a8415f5e640fa5dc79e09d2f9fd455067a4b 100644 GIT binary patch delta 1883 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 diff --git a/test/integration/image-component/static/test/index.test.js b/test/integration/image-component/static/test/index.test.js index 095dc17d65be6..50ac6890a25be 100644 --- a/test/integration/image-component/static/test/index.test.js +++ b/test/integration/image-component/static/test/index.test.js @@ -13,6 +13,22 @@ const runTests = () => { it('Should allow an image with a static src to omit height and width', async () => { expect(await browser.elementById('basic-static')).toBeTruthy() }) + it('Should automatically provide an image height and width', async () => { + expect( + await browser.elementById('basic-static').getAttribute('height') + ).toBe('300') + expect( + await browser.elementById('basic-static').getAttribute('width') + ).toBe('400') + }) + it('Should not provide image height and width for layout fill images', async () => { + expect( + await browser.elementById('fill-static').getAttribute('height') + ).toBe(undefined) + expect(await browser.elementById('fill-static').getAttribute('width')).toBe( + undefined + ) + }) } describe('Static Image Component Tests', () => { From 21400ba5c1959746b6cfd2df38cc0c188c2a5739 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 10 May 2021 20:04:40 -0700 Subject: [PATCH 03/34] Update testing for height and width --- packages/next/client/image.tsx | 5 +--- .../image-component/static/pages/index.js | 2 +- .../image-component/static/test/index.test.js | 25 ++++++++----------- yarn.lock | 14 +++++++++++ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index b30115998dc84..dac05b232e12b 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -279,11 +279,9 @@ export default function Image({ delete rest['layout'] } -<<<<<<< HEAD if (!configEnableBlurryPlaceholder) { placeholder = 'empty' } -======= const isStatic = typeof src === 'object' if (isStatic) { const staticData = src as StaticImageData @@ -292,9 +290,8 @@ export default function Image({ width = staticData.width } } - //static case is resolved, src is defintitely a string from here on. + //static case is resolved, src is definitely a string from here on. src = (isStatic ? (src as StaticImageData).src : src) as string ->>>>>>> f9bd659e4 (Add static image loading functionality with auto height and width) if (process.env.NODE_ENV !== 'production') { if (!src) { diff --git a/test/integration/image-component/static/pages/index.js b/test/integration/image-component/static/pages/index.js index 902b4121916d7..717a38c0ffa41 100644 --- a/test/integration/image-component/static/pages/index.js +++ b/test/integration/image-component/static/pages/index.js @@ -6,7 +6,7 @@ const Page = () => { return (

Static Image

- +
diff --git a/test/integration/image-component/static/test/index.test.js b/test/integration/image-component/static/test/index.test.js index 50ac6890a25be..32e73f6e2dc08 100644 --- a/test/integration/image-component/static/test/index.test.js +++ b/test/integration/image-component/static/test/index.test.js @@ -1,4 +1,10 @@ -import { findPort, killApp, nextBuild, nextStart } from 'next-test-utils' +import { + findPort, + killApp, + nextBuild, + nextStart, + renderViaHTTP, +} from 'next-test-utils' import webdriver from 'next-webdriver' import { join } from 'path' @@ -8,26 +14,14 @@ const appDir = join(__dirname, '../') let appPort let app let browser +let html const runTests = () => { it('Should allow an image with a static src to omit height and width', async () => { expect(await browser.elementById('basic-static')).toBeTruthy() }) it('Should automatically provide an image height and width', async () => { - expect( - await browser.elementById('basic-static').getAttribute('height') - ).toBe('300') - expect( - await browser.elementById('basic-static').getAttribute('width') - ).toBe('400') - }) - it('Should not provide image height and width for layout fill images', async () => { - expect( - await browser.elementById('fill-static').getAttribute('height') - ).toBe(undefined) - expect(await browser.elementById('fill-static').getAttribute('width')).toBe( - undefined - ) + expect(html).toContain('width:400px;height:300px') }) } @@ -36,6 +30,7 @@ describe('Static Image Component Tests', () => { await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort) + html = await renderViaHTTP(appPort, '/') browser = await webdriver(appPort, '/') }) afterAll(() => killApp(app)) diff --git a/yarn.lock b/yarn.lock index c163a6d979744..73b79bced5859 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8658,6 +8658,13 @@ image-size@0.9.3: dependencies: queue "6.0.1" +image-size@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.0.tgz#58b31fe4743b1cec0a0ac26f5c914d3c5b2f0750" + integrity sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw== + dependencies: + queue "6.0.2" + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -13575,6 +13582,13 @@ queue@6.0.1: dependencies: inherits "~2.0.3" +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" From c6ad7c56ecce44e50834a493b42a9d081b17a7ae Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 10 May 2021 22:49:12 -0700 Subject: [PATCH 04/34] Send cache-control immutable header for static assets --- packages/next/client/image.tsx | 13 +++++++--- .../next-server/server/image-optimizer.ts | 24 ++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index dac05b232e12b..c565dbc53bb91 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -22,6 +22,7 @@ export type ImageLoaderProps = { src: string width: number quality?: number + isStatic?: boolean } type DefaultImageLoaderProps = ImageLoaderProps & { root: string } @@ -157,6 +158,7 @@ type GenImgAttrsData = { unoptimized: boolean layout: LayoutValue loader: ImageLoader + isStatic?: boolean width?: number quality?: number sizes?: string @@ -176,6 +178,7 @@ function generateImgAttrs({ quality, sizes, loader, + isStatic, }: GenImgAttrsData): GenImgAttrsResult { if (unoptimized) { return { src, srcSet: undefined, sizes: undefined } @@ -189,7 +192,7 @@ function generateImgAttrs({ srcSet: widths .map( (w, i) => - `${loader({ src, quality, width: w })} ${ + `${loader({ src, quality, isStatic, width: w })} ${ kind === 'w' ? w : i + 1 }${kind}` ) @@ -201,7 +204,7 @@ function generateImgAttrs({ // updated by React. That causes multiple unnecessary requests if `srcSet` // and `sizes` are defined. // This bug cannot be reproduced in Chrome or Firefox. - src: loader({ src, quality, width: widths[last] }), + src: loader({ src, quality, isStatic, width: widths[last] }), } } @@ -473,6 +476,7 @@ export default function Image({ quality: qualityInt, sizes, loader, + isStatic, }) } @@ -605,6 +609,7 @@ function cloudinaryLoader({ function defaultLoader({ root, + isStatic, src, width, quality, @@ -652,5 +657,7 @@ function defaultLoader({ } } - return `${root}?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}` + return `${root}?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}${ + isStatic ? '&s=true' : '' + }` } diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 527201f213c9b..c1e54a77466c3 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -46,7 +46,7 @@ export async function imageOptimizer( } const { headers } = req - const { url, w, q } = parsedUrl.query + const { url, w, q, s } = parsedUrl.query const mimeType = getSupportedMimeType(MODERN_TYPES, headers.accept) let href: string @@ -111,6 +111,14 @@ export async function imageOptimizer( return { finished: true } } + if (s && s !== 'true') { + res.statusCode = 400 + res.end('"s" parameter must be "true" or omitted') + return { finished: true } + } + + const isStaticImage = !!s + const width = parseInt(w, 10) if (!width || isNaN(width)) { @@ -261,7 +269,7 @@ export async function imageOptimizer( ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer) if (vector || animate) { await writeToCacheDir(hashDir, upstreamType, expireAt, upstreamBuffer) - sendResponse(req, res, upstreamType, upstreamBuffer) + sendResponse(req, res, upstreamType, upstreamBuffer, isStaticImage) return { finished: true } } @@ -333,12 +341,12 @@ export async function imageOptimizer( if (optimizedBuffer) { await writeToCacheDir(hashDir, contentType, expireAt, optimizedBuffer) - sendResponse(req, res, contentType, optimizedBuffer) + sendResponse(req, res, contentType, optimizedBuffer, isStaticImage) } else { throw new Error('Unable to optimize buffer') } } catch (error) { - sendResponse(req, res, upstreamType, upstreamBuffer) + sendResponse(req, res, upstreamType, upstreamBuffer, isStaticImage) } return { finished: true } @@ -366,10 +374,14 @@ function sendResponse( req: IncomingMessage, res: ServerResponse, contentType: string | null, - buffer: Buffer + buffer: Buffer, + isStatic: boolean ) { const etag = getHash([buffer]) - res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate') + res.setHeader( + 'Cache-Control', + isStatic ? 'immutable' : 'public, max-age=0, must-revalidate' + ) if (sendEtagResponse(req, res, etag)) { return } From b5f2c12ef7d5bbfca663bdc9d3caa4eacb654740 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 10 May 2021 23:26:11 -0700 Subject: [PATCH 05/34] Add tests for static image cache control behavior --- .../image-component/static/pages/index.js | 10 +++++++--- .../image-component/static/test/index.test.js | 10 ++++++++++ .../image-optimizer/test/index.test.js | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/test/integration/image-component/static/pages/index.js b/test/integration/image-component/static/pages/index.js index 717a38c0ffa41..5251eae828b60 100644 --- a/test/integration/image-component/static/pages/index.js +++ b/test/integration/image-component/static/pages/index.js @@ -7,9 +7,13 @@ const Page = () => {

Static Image

-
- -
+
) } diff --git a/test/integration/image-component/static/test/index.test.js b/test/integration/image-component/static/test/index.test.js index 32e73f6e2dc08..dbb001b2d4353 100644 --- a/test/integration/image-component/static/test/index.test.js +++ b/test/integration/image-component/static/test/index.test.js @@ -23,6 +23,16 @@ const runTests = () => { it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') }) + it('Should append "&s=true" to URLs of static images', async () => { + expect( + await browser.elementById('basic-static').getAttribute('src') + ).toContain('&s=true') + }) + it('Should not append "&s=true" to URLs of non-static images', async () => { + expect( + await browser.elementById('basic-non-static').getAttribute('src') + ).not.toContain('&s=true') + }) } describe('Static Image Component Tests', () => { diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index d8b63b85a937b..07e8d19be2a2c 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -217,6 +217,13 @@ function runTests({ w, isDev, domains }) { ) }) + it('should fail when s is present and not "true"', async () => { + const query = { url: '/test.png', w, q: 100, s: 'foo' } + const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) + expect(res.status).toBe(400) + expect(await res.text()).toBe(`"s" parameter must be "true" or omitted`) + }) + it('should fail when domain is not defined in next.config.js', async () => { const url = `http://vercel.com/button` const query = { url, w, q: 100 } @@ -503,6 +510,14 @@ function runTests({ w, isDev, domains }) { expect(colorType).toBe(4) }) + it('should set cache-control to immutable for static images', async () => { + const query = { url: '/test.jpg', w, q: 100, s: 'true' } + const opts = { headers: { accept: 'image/webp' } } + const res = await fetchViaHTTP(appPort, '/_next/image', query, opts) + expect(res.status).toBe(200) + expect(res.headers.get('cache-control')).toBe('immutable') + }) + it("should error if the resource isn't a valid image", async () => { const query = { url: '/test.txt', w, q: 80 } const opts = { headers: { accept: 'image/webp' } } From bc5c64a6b67bbc233e9c66e6009ac68e18f724a9 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 11 May 2021 00:03:17 -0700 Subject: [PATCH 06/34] Add test for build breaking on bad image path --- .../image-component/static/test/index.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/integration/image-component/static/test/index.test.js b/test/integration/image-component/static/test/index.test.js index dbb001b2d4353..d3dd2dda5aeb2 100644 --- a/test/integration/image-component/static/test/index.test.js +++ b/test/integration/image-component/static/test/index.test.js @@ -4,6 +4,7 @@ import { nextBuild, nextStart, renderViaHTTP, + File, } from 'next-test-utils' import webdriver from 'next-webdriver' import { join } from 'path' @@ -16,6 +17,8 @@ let app let browser let html +const indexPage = new File(join(appDir, 'pages/index.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() @@ -35,6 +38,21 @@ const runTests = () => { }) } +describe('Build Error Tests', () => { + 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', () => { beforeAll(async () => { await nextBuild(appDir) From aa9e4fdd7d3dbd65eb7efd974e6722d29b9cea01 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 11 May 2021 00:31:25 -0700 Subject: [PATCH 07/34] Move static image tests into default test suite and add format test --- .../pages/index.js => default/pages/static.js} | 10 ++++++++++ .../{static => default}/public/foo/test-rect.jpg | Bin .../index.test.js => default/test/static.test.js} | 10 +++++++--- 3 files changed, 17 insertions(+), 3 deletions(-) rename test/integration/image-component/{static/pages/index.js => default/pages/static.js} (56%) rename test/integration/image-component/{static => default}/public/foo/test-rect.jpg (100%) rename test/integration/image-component/{static/test/index.test.js => default/test/static.test.js} (79%) diff --git a/test/integration/image-component/static/pages/index.js b/test/integration/image-component/default/pages/static.js similarity index 56% rename from test/integration/image-component/static/pages/index.js rename to test/integration/image-component/default/pages/static.js index 5251eae828b60..228f55062a7dd 100644 --- a/test/integration/image-component/static/pages/index.js +++ b/test/integration/image-component/default/pages/static.js @@ -2,6 +2,13 @@ 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 testSVG from '../public/test.svg' +import testGIF from '../public/test.gif' + +const testFiles = [testJPG, testPNG, testSVG, testGIF] + const Page = () => { return (
@@ -14,6 +21,9 @@ const Page = () => { height="300" layout="fixed" /> + {testFiles.map((f, i) => ( + + ))}
) } diff --git a/test/integration/image-component/static/public/foo/test-rect.jpg b/test/integration/image-component/default/public/foo/test-rect.jpg similarity index 100% rename from test/integration/image-component/static/public/foo/test-rect.jpg rename to test/integration/image-component/default/public/foo/test-rect.jpg diff --git a/test/integration/image-component/static/test/index.test.js b/test/integration/image-component/default/test/static.test.js similarity index 79% rename from test/integration/image-component/static/test/index.test.js rename to test/integration/image-component/default/test/static.test.js index d3dd2dda5aeb2..14aa1ac115875 100644 --- a/test/integration/image-component/static/test/index.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -17,11 +17,15 @@ let app let browser let html -const indexPage = new File(join(appDir, 'pages/index.js')) +const indexPage = new File(join(appDir, 'pages/static.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('format-test-0')).toBeTruthy() + expect(await browser.elementById('format-test-1')).toBeTruthy() + expect(await browser.elementById('format-test-2')).toBeTruthy() + expect(await browser.elementById('format-test-3')).toBeTruthy() }) it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') @@ -58,8 +62,8 @@ describe('Static Image Component Tests', () => { await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort) - html = await renderViaHTTP(appPort, '/') - browser = await webdriver(appPort, '/') + html = await renderViaHTTP(appPort, '/static') + browser = await webdriver(appPort, '/static') }) afterAll(() => killApp(app)) runTests() From 9b4c5135f48195132420dda80e2b2fa8794e98a2 Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 11 May 2021 01:57:36 -0700 Subject: [PATCH 08/34] Put static image behind experimental flag --- packages/next/build/webpack-config.ts | 12 ++++--- .../webpack/loaders/next-image-loader.js | 12 +++++-- .../next/next-server/server/config-shared.ts | 2 ++ .../default/test/index.test.js | 31 +++++++++++++++++-- .../default/test/static.test.js | 16 +++++++++- 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 6b52316db94fd..958c681aaf46e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -987,10 +987,14 @@ export default async function getBaseWebpackConfig( ] : defaultLoaders.babel, }, - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - loader: 'next-image-loader', - }, + ...(config.experimental.staticImages + ? [ + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + loader: 'next-image-loader', + }, + ] + : []), ].filter(Boolean), }, plugins: [ diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index d9ce3115db41d..6f22969b12cc8 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -14,16 +14,24 @@ function nextImageLoader(content) { '/[path][name].[ext]', opts ) - //TODO: TEST FOR URL WITHOUT /public + + if (interpolatedName.slice(0, 7) !== '/public') { + const err = new Error( + 'Static Image loader used with filepath not in the /public directory: ' + + interpolatedName + ) + this.emitError(err) + } + const src = interpolatedName.slice(7) const imageSize = sizeOf(this.resourcePath) - const esModule = typeof query.esModule !== 'undefined' ? query.esModule : true const stringifiedData = JSON.stringify({ src, height: imageSize.height, width: imageSize.width, }) + const esModule = typeof query.esModule !== 'undefined' ? query.esModule : true return `${ esModule ? 'export default' : 'module.exports =' } ${stringifiedData};` diff --git a/packages/next/next-server/server/config-shared.ts b/packages/next/next-server/server/config-shared.ts index fb45863f3269e..3070306dd6246 100644 --- a/packages/next/next-server/server/config-shared.ts +++ b/packages/next/next-server/server/config-shared.ts @@ -45,6 +45,7 @@ export type NextConfig = { [key: string]: any } & { workerThreads?: boolean pageEnv?: boolean optimizeImages?: boolean + staticImages?: boolean optimizeCss?: boolean scrollRestoration?: boolean scriptLoader?: boolean @@ -108,6 +109,7 @@ export const defaultConfig: NextConfig = { workerThreads: false, pageEnv: false, optimizeImages: false, + staticImages: false, optimizeCss: false, scrollRestoration: false, scriptLoader: false, diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js index 6460efa45ac25..09c16a313bc9f 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -562,21 +562,47 @@ function runTests(mode) { describe('Image Component Tests', () => { describe('dev mode', () => { beforeAll(async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { + experimental: { + staticImages: true + }, + } + ` + ) appPort = await findPort() app = await launchApp(appDir, appPort) }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.unlink(nextConfig) + await killApp(app) + }) runTests('dev') }) describe('server mode', () => { beforeAll(async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { + experimental: { + staticImages: true + }, + } + ` + ) await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort) }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.unlink(nextConfig) + await killApp(app) + }) runTests('server') }) @@ -590,6 +616,7 @@ describe('Image Component Tests', () => { target: 'serverless', experimental: { enableBlurryPlaceholder: true, + staticImages: true }, } ` diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js index 14aa1ac115875..2968f2ed36718 100644 --- a/test/integration/image-component/default/test/static.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -18,6 +18,7 @@ let browser let html const indexPage = new File(join(appDir, 'pages/static.js')) +const nextConfig = new File(join(appDir, 'next.config.js')) const runTests = () => { it('Should allow an image with a static src to omit height and width', async () => { @@ -59,12 +60,25 @@ describe('Build Error Tests', () => { }) describe('Static Image Component Tests', () => { beforeAll(async () => { + nextConfig.write( + ` + module.exports = { + experimental: { + staticImages: true, + }, + } + ` + ) + await nextBuild(appDir) appPort = await findPort() app = await nextStart(appDir, appPort) html = await renderViaHTTP(appPort, '/static') browser = await webdriver(appPort, '/static') }) - afterAll(() => killApp(app)) + afterAll(() => { + nextConfig.delete() + killApp(app) + }) runTests() }) From fdd27c4df681a38fa6a718fdf1428917dcf57d8d Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 11 May 2021 03:05:51 -0700 Subject: [PATCH 09/34] Fix support for images loaded with Require --- packages/next/client/image.tsx | 39 ++++++++++++++++--- .../image-component/default/pages/static.js | 5 +++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index c565dbc53bb91..fce8ebd4049f2 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -56,11 +56,17 @@ interface StaticImageData { width: number } +interface StaticRequire { + default: StaticImageData +} + +type StaticImport = StaticRequire | StaticImageData + export type ImageProps = Omit< JSX.IntrinsicElements['img'], 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' > & { - src: string | StaticImageData + src: string | StaticImport loader?: ImageLoader quality?: number | string priority?: boolean @@ -286,15 +292,36 @@ export default function Image({ placeholder = 'empty' } const isStatic = typeof src === 'object' + let staticSrc = '' if (isStatic) { - const staticData = src as StaticImageData + let staticImport = src as StaticImport + let staticImageData: StaticImageData + if ((staticImport as StaticRequire).default) { + staticImageData = (staticImport as StaticRequire).default + } else { + staticImageData = staticImport as StaticImageData + } + if (!staticImageData.src) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( + staticImageData + )}` + ) + } + staticSrc = staticImageData.src if (!layout || layout !== 'fill') { - height = staticData.height - width = staticData.width + height = staticImageData.height + width = staticImageData.width + if (!height || !width) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( + staticImageData + )}` + ) + } } } - //static case is resolved, src is definitely a string from here on. - src = (isStatic ? (src as StaticImageData).src : src) as string + src = (isStatic ? staticSrc : src) as string if (process.env.NODE_ENV !== 'production') { if (!src) { diff --git a/test/integration/image-component/default/pages/static.js b/test/integration/image-component/default/pages/static.js index 228f55062a7dd..e00219b11a111 100644 --- a/test/integration/image-component/default/pages/static.js +++ b/test/integration/image-component/default/pages/static.js @@ -14,6 +14,11 @@ const Page = () => {

Static Image

+ Date: Tue, 11 May 2021 09:37:55 -0700 Subject: [PATCH 10/34] Remove extraneous code from next-image-loader --- .../next/build/webpack/loaders/next-image-loader.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index 6f22969b12cc8..af44ffa2bf967 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -5,10 +5,8 @@ function nextImageLoader(content) { this.cacheable && this.cacheable(true) this.addDependency(this.resourcePath) - const query = loaderUtils.getOptions(this) - const context = query.context || this.rootContext || this.options.context - const regExp = query.regExp - const opts = { context, content, regExp } + const context = this.rootContext + const opts = { context, content } const interpolatedName = loaderUtils.interpolateName( this, '/[path][name].[ext]', @@ -30,11 +28,7 @@ function nextImageLoader(content) { height: imageSize.height, width: imageSize.width, }) - - const esModule = typeof query.esModule !== 'undefined' ? query.esModule : true - return `${ - esModule ? 'export default' : 'module.exports =' - } ${stringifiedData};` + return `${'export default '} ${stringifiedData};` } export default nextImageLoader From ccefcb2dc2b2c6a13a68ad607e95788a5b82b52b Mon Sep 17 00:00:00 2001 From: atcastle Date: Wed, 12 May 2021 23:45:57 -0700 Subject: [PATCH 11/34] Initial addition of new loader for converting new URL() usage --- .../babel/plugins/image-import-transforms.ts | 69 +++++++++++++++++++ packages/next/build/babel/preset.ts | 1 + packages/next/client/image.tsx | 2 +- 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/next/build/babel/plugins/image-import-transforms.ts diff --git a/packages/next/build/babel/plugins/image-import-transforms.ts b/packages/next/build/babel/plugins/image-import-transforms.ts new file mode 100644 index 0000000000000..b8f19e9a7e928 --- /dev/null +++ b/packages/next/build/babel/plugins/image-import-transforms.ts @@ -0,0 +1,69 @@ +import { PluginObj, types } from 'next/dist/compiled/babel/core' + +type UrlUsageData = { + url: string + metaName: string +} + +const getNewUrlSrc = (attribute: types.JSXAttribute) => { + return ( + attribute.name.name === 'src' && + (attribute.value as types.JSXExpressionContainer).expression && + (attribute.value as types.JSXExpressionContainer).expression.type === + 'NewExpression' && + (((attribute.value as types.JSXExpressionContainer) + .expression as types.NewExpression).callee as types.Identifier).name && + (((attribute.value as types.JSXExpressionContainer) + .expression as types.NewExpression).arguments[0] as types.StringLiteral) + .value + ) +} + +export default function ({ types: t }: { types: typeof types }): PluginObj { + let imageComponent: string | null = null + let newUrlUsages: UrlUsageData[] = [] + return { + visitor: { + Program: { + exit(path) { + const newImportStatements = newUrlUsages.map((urlUsage) => { + return t.importDeclaration( + [t.importDefaultSpecifier(t.identifier(urlUsage.metaName))], + t.stringLiteral(urlUsage.url) + ) + }) + path.unshiftContainer('body', newImportStatements) + }, + }, + ImportDeclaration(path) { + if (path.node.source.value === 'next/image') { + imageComponent = path.node.specifiers[0].local.name + } + }, + JSXOpeningElement(path) { + if ( + !imageComponent || + (path.node.name as types.JSXIdentifier).name !== imageComponent + ) { + return + } + const attributes = path.node.attributes + + for (let attribute of attributes) { + let url = getNewUrlSrc(attribute as types.JSXAttribute) + if (url) { + const metaName = 'meta' + newUrlUsages.length + newUrlUsages.push({ url, metaName }) + attributes.push( + t.jsxAttribute( + t.jsxIdentifier('meta'), + t.jsxExpressionContainer(t.identifier(metaName)) + ) + ) + break + } + } + }, + }, + } +} diff --git a/packages/next/build/babel/preset.ts b/packages/next/build/babel/preset.ts index 04a9aee246dd9..8919291c0df80 100644 --- a/packages/next/build/babel/preset.ts +++ b/packages/next/build/babel/preset.ts @@ -168,6 +168,7 @@ export default ( lib: true, }, ], + require('./plugins/image-import-transforms'), require('next/dist/compiled/babel/plugin-syntax-dynamic-import'), require('./plugins/react-loadable-plugin'), [ diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index fce8ebd4049f2..d5e777c86dc4b 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -304,7 +304,7 @@ export default function Image({ if (!staticImageData.src) { throw new Error( `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( - staticImageData + staticImport )}` ) } From acd4ae31b24645f3633f5d5c1928124b12f44251 Mon Sep 17 00:00:00 2001 From: atcastle Date: Fri, 14 May 2021 10:55:41 -0700 Subject: [PATCH 12/34] Change static query parameter value from true to 1 --- packages/next/client/image.tsx | 2 +- packages/next/next-server/server/image-optimizer.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index d5e777c86dc4b..af8af82dccf35 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -685,6 +685,6 @@ function defaultLoader({ } return `${root}?url=${encodeURIComponent(src)}&w=${width}&q=${quality || 75}${ - isStatic ? '&s=true' : '' + isStatic ? '&s=1' : '' }` } diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index c1e54a77466c3..5b85d2ded7366 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -111,9 +111,9 @@ export async function imageOptimizer( return { finished: true } } - if (s && s !== 'true') { + if (s && s !== '1') { res.statusCode = 400 - res.end('"s" parameter must be "true" or omitted') + res.end('"s" parameter must be "1" or omitted') return { finished: true } } From 5a8ce4cbb1943252ef929e5b690b5c351b6680b6 Mon Sep 17 00:00:00 2001 From: atcastle Date: Fri, 14 May 2021 16:07:54 -0700 Subject: [PATCH 13/34] Update tests for static query param --- .../image-component/default/test/static.test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js index 2968f2ed36718..30d137a41b8b4 100644 --- a/test/integration/image-component/default/test/static.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -31,15 +31,15 @@ const runTests = () => { it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') }) - it('Should append "&s=true" to URLs of static images', async () => { + it('Should append "&s=1" to URLs of static images', async () => { expect( await browser.elementById('basic-static').getAttribute('src') - ).toContain('&s=true') + ).toContain('&s=1') }) - it('Should not append "&s=true" to URLs of non-static images', async () => { + it('Should not append "&s=1" to URLs of non-static images', async () => { expect( await browser.elementById('basic-non-static').getAttribute('src') - ).not.toContain('&s=true') + ).not.toContain('&s=1') }) } From 29892e3dade2b4f0b47d9d10d1bb855ef1fc980b Mon Sep 17 00:00:00 2001 From: atcastle Date: Tue, 18 May 2021 10:00:11 -0700 Subject: [PATCH 14/34] Support new URL constructor for image component --- .../babel/plugins/image-import-transforms.ts | 7 ++ packages/next/build/babel/preset.ts | 2 +- packages/next/build/webpack-config.ts | 1 + packages/next/client/image.tsx | 62 ++++++++++++------ packages/next/types/webpack.d.ts | 2 + .../image-component/default/pages/static.js | 5 ++ .../default/public/foo/test.gif | Bin 0 -> 2301 bytes .../default/test/static.test.js | 3 + 8 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 test/integration/image-component/default/public/foo/test.gif diff --git a/packages/next/build/babel/plugins/image-import-transforms.ts b/packages/next/build/babel/plugins/image-import-transforms.ts index b8f19e9a7e928..01a20345da2bd 100644 --- a/packages/next/build/babel/plugins/image-import-transforms.ts +++ b/packages/next/build/babel/plugins/image-import-transforms.ts @@ -19,6 +19,10 @@ const getNewUrlSrc = (attribute: types.JSXAttribute) => { ) } +const transformPublicImagePath = (publicPath: string): string => { + return publicPath.replace('../public', '') +} + export default function ({ types: t }: { types: typeof types }): PluginObj { let imageComponent: string | null = null let newUrlUsages: UrlUsageData[] = [] @@ -54,6 +58,9 @@ export default function ({ types: t }: { types: typeof types }): PluginObj { if (url) { const metaName = 'meta' + newUrlUsages.length newUrlUsages.push({ url, metaName }) + ;(attribute as types.JSXAttribute).value = t.stringLiteral( + transformPublicImagePath(url) + ) attributes.push( t.jsxAttribute( t.jsxIdentifier('meta'), diff --git a/packages/next/build/babel/preset.ts b/packages/next/build/babel/preset.ts index 8919291c0df80..3b11298dd957f 100644 --- a/packages/next/build/babel/preset.ts +++ b/packages/next/build/babel/preset.ts @@ -168,7 +168,6 @@ export default ( lib: true, }, ], - require('./plugins/image-import-transforms'), require('next/dist/compiled/babel/plugin-syntax-dynamic-import'), require('./plugins/react-loadable-plugin'), [ @@ -212,6 +211,7 @@ export default ( // smaller. require('next/dist/compiled/babel/plugin-proposal-numeric-separator'), require('next/dist/compiled/babel/plugin-proposal-export-namespace-from'), + require('./plugins/image-import-transforms'), ].filter(Boolean), } } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 958c681aaf46e..71e1ecf4d3ef3 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -924,6 +924,7 @@ export default async function getBaseWebpackConfig( : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ dev ? '[name]' : '[name].[contenthash]' }.js`, + assetModuleFilename: 'static/[contenthash].[ext]', strictModuleExceptionHandling: true, crossOriginLoading: crossOrigin, futureEmitAssets: !dev, diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index af8af82dccf35..16ec708c594a1 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -50,23 +50,36 @@ type PlaceholderValue = 'blur' | 'empty' type ImgElementStyle = NonNullable -interface StaticImageData { - src: string +interface ImageMetaData { height: number width: number } +type StaticImageData = { + src: string +} & ImageMetaData + interface StaticRequire { default: StaticImageData } type StaticImport = StaticRequire | StaticImageData +function isStaticRequire( + data: StaticRequire | StaticImageData +): data is StaticRequire { + return data && (data as StaticRequire).default !== undefined +} + +function isUrl(data: string | StaticImport | URL): data is URL { + return !!data && (data as URL).href !== undefined +} + export type ImageProps = Omit< JSX.IntrinsicElements['img'], 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' > & { - src: string | StaticImport + src: string | StaticImport | URL loader?: ImageLoader quality?: number | string priority?: boolean @@ -74,6 +87,7 @@ export type ImageProps = Omit< unoptimized?: boolean objectFit?: ImgElementStyle['objectFit'] objectPosition?: ImgElementStyle['objectPosition'] + meta?: ImageMetaData } & ( | { width?: never @@ -271,6 +285,7 @@ export default function Image({ loader = defaultImageLoader, placeholder = 'empty', blurDataURL, + meta, ...all }: ImageProps) { let rest: Partial = all @@ -291,37 +306,42 @@ export default function Image({ if (!configEnableBlurryPlaceholder) { placeholder = 'empty' } - const isStatic = typeof src === 'object' + height = height || meta?.height + width = width || meta?.width + let isStatic = false let staticSrc = '' - if (isStatic) { + if (typeof src !== 'string' && typeof src !== 'undefined') { + isStatic = true + // Handle static image import let staticImport = src as StaticImport let staticImageData: StaticImageData - if ((staticImport as StaticRequire).default) { - staticImageData = (staticImport as StaticRequire).default + if (isStaticRequire(staticImport)) { + staticImageData = staticImport.default } else { - staticImageData = staticImport as StaticImageData + staticImageData = staticImport } - if (!staticImageData.src) { + staticSrc = isUrl(src) ? src.href : staticImageData.src + + height = height || staticImageData.height + width = width || staticImageData.width + + if (!staticSrc) { throw new Error( `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( staticImport )}` ) } - staticSrc = staticImageData.src - if (!layout || layout !== 'fill') { - height = staticImageData.height - width = staticImageData.width - if (!height || !width) { - throw new Error( - `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( - staticImageData - )}` - ) - } + + if ((!layout || layout !== 'fill') && (!height || !width)) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( + staticImageData + )}` + ) } } - src = (isStatic ? staticSrc : src) as string + src = isStatic ? staticSrc : (src as string) if (process.env.NODE_ENV !== 'production') { if (!src) { diff --git a/packages/next/types/webpack.d.ts b/packages/next/types/webpack.d.ts index ae051c3e6aa88..7a6bec713e42d 100644 --- a/packages/next/types/webpack.d.ts +++ b/packages/next/types/webpack.d.ts @@ -176,6 +176,8 @@ declare module 'webpack' { filename?: string | Function /** The filename of non-entry chunks as relative path inside the output.path directory. */ chunkFilename?: string + /** The filename of asset modules as relative path inside the output.path */ + assetModuleFilename: string /** Number of milliseconds before chunk request expires, defaults to 120,000. */ chunkLoadTimeout?: number /** This option enables cross-origin loading of chunks. */ diff --git a/test/integration/image-component/default/pages/static.js b/test/integration/image-component/default/pages/static.js index e00219b11a111..83833adc81c99 100644 --- a/test/integration/image-component/default/pages/static.js +++ b/test/integration/image-component/default/pages/static.js @@ -19,6 +19,11 @@ const Page = () => { src={require('../public/foo/test-rect.jpg')} layout="fixed" /> + =R*$unic1 zg2UmkSghRKTw7b)!NEZl6%~k--QEQpxtSx3ymaZBp}wJpx`rkP0G>n)3hT8?9RRF{ zHxAl>|J%|E&OZZof&D-b90f}DC@;^8rl!W+-oKA8Z^4g~2h_N|eqZxHSz$-C7YYEF z092uk^2B*VSP#M`e%_v3oCslYl#4xddl(7AsyOI^5bovj4nOe>7d!vNEH1{GU%Ld& zCIVr&^G~eG#ST9)cNRFx1;@ccMHuMgC>G-bUE)V@M-f4LTsMQV^pE8Qt^q@!58%K7 z_yAXc0p36j${vtuzt?I0(3ybSkOl?4JwX7#K?-N!0%=tt)(3b2G?bkn9t}Mhr~(SE z`P>Y^FH^mow7C7CnMI5M*gehRFqHw|&jjFmG>5ZOz~Ou^1b}x0fTomR`r9dxpDPfb z_=`K54Zz;}0MK6k$RGls2%Wdr8Zrk8?%xb}*#dQAZ1oOg=?m>8Mm@()Dc;dF z$EBnUv-HwawZ|Jf;a|2Iw90A9^F*i~OlT+J=osDHUaGgz6RYx($G)A*fsKH~M zd*-VG86Nc$YtMsB%HOZD8t(P1?r=iG!tX~!Mnyk}!9R?Ri%&>=l$4x8NKH%6AZBJ| z=j7()7Zj3;$P{XEN$KM!W#tu>PoGs)(`ugA*3~z>c-h$Ws=1}L?e&{>dPip$qr0cK zuYX{0=xyV3V!AI2vpr~WfN^Ko`=eqnLx)AGvd+WH1_^YfQ2*7nz(Z{OJ*z$+w; zsK|SDUqDRRy1gR5C0bP8C<{?p(DqPD%WbTkU0L`h0e%TDjeJU?Co7qiSks>tb*8DK zIgQ6+fO*ZTK!V*gY-Ak`hp_zuZYuwxfmd<|)KFy5B4f zWrm|Mt!oo+Fp_364&rz0dTh2m`1-}Q99wx7_YJ7*DdkZT#q9m ztZ%~OpF(WUFXco|vxXFgZ@_GW*xwp$F4p7YZ<&}96=0&3s_22-gc zApy6@lMzztKItJW2AU8d?a?Id#_xp4aZvEJYr;yBSsBjsYJXUeW>kAYoRo2z{M`_2 zJBoJ?J6$g3(s|R#`vy4@R!RFrB~i(*7q08YnI89)BMjYAgr{r~+R%h>ok)e0sA6q$ z(2HO?D$?CSAR|#aRe};*j4lRQezSNm;FvEoYAIE4jf#8Jy;u_VpgZbD zUL9E|&y1*P;drB7pG}oZs$0*%B)2txoO)(H%h&WIq6mpit`MTk)5di1DzTGf7mISw zC;MG$b?S<4-hcJ<%D7qAXryHA2HV-9>KrWdL{4}d?PDV_8yya2?K>yqHnQ(jMYFDx zL3)BH%1Ea1)H_$ZwvyTFC$2v6>8K1UBOFW(Syxsqt+t%f)=c}bPvK*ydZx~G(n+tF z6=U%^Yqi&^u5(&7p}}2RPygJzoLR41R8!V}fmof~Xkmi;=Y2f4ps_G~Pb(SZ+Wg8}I!=qTKzY(cWW+zYOea*<8j485+wQ zn;)~$dMEq&lXjmXyWIQJ%pLaO?eX*$>usc#@c4GW%$aX}Erm5pXd`=kR^5licR@+C ztBvulna6b8&klPLN$V?qv&f{Crit3qm#ZanYZoM3OB?bMuXwYV!T7s%)Il-qnn9}5 z5ofZ@oj}^wZvH%-%lq*r_eK#|?~iO6-b=E=Lt=Wn1S_2$#n_w|ecd9TROY_> zVzyYVgsM{b^#1^SKBRj1|H&UN>6&?4ZciMd2NNW-y zt4AZd91b$m+l|SZY4k29U1mmaEcqk_$#An`5=Xkor)%g8k3eSTqzFa(4YC_YWM<79 zQ*?*v-M)3q?6p|RxjAG{;3zYP)kQhKMen)ym6;3nQ1$qYdczNH<_Y|hsxR5m8>PFM tuP7Qvy?o554{xoayL`58Nz-? { expect(await browser.elementById('format-test-2')).toBeTruthy() expect(await browser.elementById('format-test-3')).toBeTruthy() }) + it('Should allow an image with an in-line URL object src to omit height and width', async () => { + expect(await browser.elementById('new-url-static')).toBeTruthy() + }) it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') }) From 108aed931d16928eb6d25230c341e51819c7e1c1 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 08:34:08 -0700 Subject: [PATCH 15/34] Revert "Support new URL constructor for image component" This reverts commit 29892e3dade2b4f0b47d9d10d1bb855ef1fc980b. --- .../babel/plugins/image-import-transforms.ts | 7 -- packages/next/build/babel/preset.ts | 2 +- packages/next/build/webpack-config.ts | 1 - packages/next/client/image.tsx | 62 ++++++------------ packages/next/types/webpack.d.ts | 2 - .../image-component/default/pages/static.js | 5 -- .../default/public/foo/test.gif | Bin 2301 -> 0 bytes .../default/test/static.test.js | 3 - 8 files changed, 22 insertions(+), 60 deletions(-) delete mode 100644 test/integration/image-component/default/public/foo/test.gif diff --git a/packages/next/build/babel/plugins/image-import-transforms.ts b/packages/next/build/babel/plugins/image-import-transforms.ts index 01a20345da2bd..b8f19e9a7e928 100644 --- a/packages/next/build/babel/plugins/image-import-transforms.ts +++ b/packages/next/build/babel/plugins/image-import-transforms.ts @@ -19,10 +19,6 @@ const getNewUrlSrc = (attribute: types.JSXAttribute) => { ) } -const transformPublicImagePath = (publicPath: string): string => { - return publicPath.replace('../public', '') -} - export default function ({ types: t }: { types: typeof types }): PluginObj { let imageComponent: string | null = null let newUrlUsages: UrlUsageData[] = [] @@ -58,9 +54,6 @@ export default function ({ types: t }: { types: typeof types }): PluginObj { if (url) { const metaName = 'meta' + newUrlUsages.length newUrlUsages.push({ url, metaName }) - ;(attribute as types.JSXAttribute).value = t.stringLiteral( - transformPublicImagePath(url) - ) attributes.push( t.jsxAttribute( t.jsxIdentifier('meta'), diff --git a/packages/next/build/babel/preset.ts b/packages/next/build/babel/preset.ts index 3b11298dd957f..8919291c0df80 100644 --- a/packages/next/build/babel/preset.ts +++ b/packages/next/build/babel/preset.ts @@ -168,6 +168,7 @@ export default ( lib: true, }, ], + require('./plugins/image-import-transforms'), require('next/dist/compiled/babel/plugin-syntax-dynamic-import'), require('./plugins/react-loadable-plugin'), [ @@ -211,7 +212,6 @@ export default ( // smaller. require('next/dist/compiled/babel/plugin-proposal-numeric-separator'), require('next/dist/compiled/babel/plugin-proposal-export-namespace-from'), - require('./plugins/image-import-transforms'), ].filter(Boolean), } } diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 71e1ecf4d3ef3..958c681aaf46e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -924,7 +924,6 @@ export default async function getBaseWebpackConfig( : `static/chunks/${isDevFallback ? 'fallback/' : ''}${ dev ? '[name]' : '[name].[contenthash]' }.js`, - assetModuleFilename: 'static/[contenthash].[ext]', strictModuleExceptionHandling: true, crossOriginLoading: crossOrigin, futureEmitAssets: !dev, diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 16ec708c594a1..af8af82dccf35 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -50,36 +50,23 @@ type PlaceholderValue = 'blur' | 'empty' type ImgElementStyle = NonNullable -interface ImageMetaData { +interface StaticImageData { + src: string height: number width: number } -type StaticImageData = { - src: string -} & ImageMetaData - interface StaticRequire { default: StaticImageData } type StaticImport = StaticRequire | StaticImageData -function isStaticRequire( - data: StaticRequire | StaticImageData -): data is StaticRequire { - return data && (data as StaticRequire).default !== undefined -} - -function isUrl(data: string | StaticImport | URL): data is URL { - return !!data && (data as URL).href !== undefined -} - export type ImageProps = Omit< JSX.IntrinsicElements['img'], 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' > & { - src: string | StaticImport | URL + src: string | StaticImport loader?: ImageLoader quality?: number | string priority?: boolean @@ -87,7 +74,6 @@ export type ImageProps = Omit< unoptimized?: boolean objectFit?: ImgElementStyle['objectFit'] objectPosition?: ImgElementStyle['objectPosition'] - meta?: ImageMetaData } & ( | { width?: never @@ -285,7 +271,6 @@ export default function Image({ loader = defaultImageLoader, placeholder = 'empty', blurDataURL, - meta, ...all }: ImageProps) { let rest: Partial = all @@ -306,42 +291,37 @@ export default function Image({ if (!configEnableBlurryPlaceholder) { placeholder = 'empty' } - height = height || meta?.height - width = width || meta?.width - let isStatic = false + const isStatic = typeof src === 'object' let staticSrc = '' - if (typeof src !== 'string' && typeof src !== 'undefined') { - isStatic = true - // Handle static image import + if (isStatic) { let staticImport = src as StaticImport let staticImageData: StaticImageData - if (isStaticRequire(staticImport)) { - staticImageData = staticImport.default + if ((staticImport as StaticRequire).default) { + staticImageData = (staticImport as StaticRequire).default } else { - staticImageData = staticImport + staticImageData = staticImport as StaticImageData } - staticSrc = isUrl(src) ? src.href : staticImageData.src - - height = height || staticImageData.height - width = width || staticImageData.width - - if (!staticSrc) { + if (!staticImageData.src) { throw new Error( `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( staticImport )}` ) } - - if ((!layout || layout !== 'fill') && (!height || !width)) { - throw new Error( - `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( - staticImageData - )}` - ) + staticSrc = staticImageData.src + if (!layout || layout !== 'fill') { + height = staticImageData.height + width = staticImageData.width + if (!height || !width) { + throw new Error( + `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( + staticImageData + )}` + ) + } } } - src = isStatic ? staticSrc : (src as string) + src = (isStatic ? staticSrc : src) as string if (process.env.NODE_ENV !== 'production') { if (!src) { diff --git a/packages/next/types/webpack.d.ts b/packages/next/types/webpack.d.ts index 7a6bec713e42d..ae051c3e6aa88 100644 --- a/packages/next/types/webpack.d.ts +++ b/packages/next/types/webpack.d.ts @@ -176,8 +176,6 @@ declare module 'webpack' { filename?: string | Function /** The filename of non-entry chunks as relative path inside the output.path directory. */ chunkFilename?: string - /** The filename of asset modules as relative path inside the output.path */ - assetModuleFilename: string /** Number of milliseconds before chunk request expires, defaults to 120,000. */ chunkLoadTimeout?: number /** This option enables cross-origin loading of chunks. */ diff --git a/test/integration/image-component/default/pages/static.js b/test/integration/image-component/default/pages/static.js index 83833adc81c99..e00219b11a111 100644 --- a/test/integration/image-component/default/pages/static.js +++ b/test/integration/image-component/default/pages/static.js @@ -19,11 +19,6 @@ const Page = () => { src={require('../public/foo/test-rect.jpg')} layout="fixed" /> - =R*$unic1 zg2UmkSghRKTw7b)!NEZl6%~k--QEQpxtSx3ymaZBp}wJpx`rkP0G>n)3hT8?9RRF{ zHxAl>|J%|E&OZZof&D-b90f}DC@;^8rl!W+-oKA8Z^4g~2h_N|eqZxHSz$-C7YYEF z092uk^2B*VSP#M`e%_v3oCslYl#4xddl(7AsyOI^5bovj4nOe>7d!vNEH1{GU%Ld& zCIVr&^G~eG#ST9)cNRFx1;@ccMHuMgC>G-bUE)V@M-f4LTsMQV^pE8Qt^q@!58%K7 z_yAXc0p36j${vtuzt?I0(3ybSkOl?4JwX7#K?-N!0%=tt)(3b2G?bkn9t}Mhr~(SE z`P>Y^FH^mow7C7CnMI5M*gehRFqHw|&jjFmG>5ZOz~Ou^1b}x0fTomR`r9dxpDPfb z_=`K54Zz;}0MK6k$RGls2%Wdr8Zrk8?%xb}*#dQAZ1oOg=?m>8Mm@()Dc;dF z$EBnUv-HwawZ|Jf;a|2Iw90A9^F*i~OlT+J=osDHUaGgz6RYx($G)A*fsKH~M zd*-VG86Nc$YtMsB%HOZD8t(P1?r=iG!tX~!Mnyk}!9R?Ri%&>=l$4x8NKH%6AZBJ| z=j7()7Zj3;$P{XEN$KM!W#tu>PoGs)(`ugA*3~z>c-h$Ws=1}L?e&{>dPip$qr0cK zuYX{0=xyV3V!AI2vpr~WfN^Ko`=eqnLx)AGvd+WH1_^YfQ2*7nz(Z{OJ*z$+w; zsK|SDUqDRRy1gR5C0bP8C<{?p(DqPD%WbTkU0L`h0e%TDjeJU?Co7qiSks>tb*8DK zIgQ6+fO*ZTK!V*gY-Ak`hp_zuZYuwxfmd<|)KFy5B4f zWrm|Mt!oo+Fp_364&rz0dTh2m`1-}Q99wx7_YJ7*DdkZT#q9m ztZ%~OpF(WUFXco|vxXFgZ@_GW*xwp$F4p7YZ<&}96=0&3s_22-gc zApy6@lMzztKItJW2AU8d?a?Id#_xp4aZvEJYr;yBSsBjsYJXUeW>kAYoRo2z{M`_2 zJBoJ?J6$g3(s|R#`vy4@R!RFrB~i(*7q08YnI89)BMjYAgr{r~+R%h>ok)e0sA6q$ z(2HO?D$?CSAR|#aRe};*j4lRQezSNm;FvEoYAIE4jf#8Jy;u_VpgZbD zUL9E|&y1*P;drB7pG}oZs$0*%B)2txoO)(H%h&WIq6mpit`MTk)5di1DzTGf7mISw zC;MG$b?S<4-hcJ<%D7qAXryHA2HV-9>KrWdL{4}d?PDV_8yya2?K>yqHnQ(jMYFDx zL3)BH%1Ea1)H_$ZwvyTFC$2v6>8K1UBOFW(Syxsqt+t%f)=c}bPvK*ydZx~G(n+tF z6=U%^Yqi&^u5(&7p}}2RPygJzoLR41R8!V}fmof~Xkmi;=Y2f4ps_G~Pb(SZ+Wg8}I!=qTKzY(cWW+zYOea*<8j485+wQ zn;)~$dMEq&lXjmXyWIQJ%pLaO?eX*$>usc#@c4GW%$aX}Erm5pXd`=kR^5licR@+C ztBvulna6b8&klPLN$V?qv&f{Crit3qm#ZanYZoM3OB?bMuXwYV!T7s%)Il-qnn9}5 z5ofZ@oj}^wZvH%-%lq*r_eK#|?~iO6-b=E=Lt=Wn1S_2$#n_w|ecd9TROY_> zVzyYVgsM{b^#1^SKBRj1|H&UN>6&?4ZciMd2NNW-y zt4AZd91b$m+l|SZY4k29U1mmaEcqk_$#An`5=Xkor)%g8k3eSTqzFa(4YC_YWM<79 zQ*?*v-M)3q?6p|RxjAG{;3zYP)kQhKMen)ym6;3nQ1$qYdczNH<_Y|hsxR5m8>PFM tuP7Qvy?o554{xoayL`58Nz-? { expect(await browser.elementById('format-test-2')).toBeTruthy() expect(await browser.elementById('format-test-3')).toBeTruthy() }) - it('Should allow an image with an in-line URL object src to omit height and width', async () => { - expect(await browser.elementById('new-url-static')).toBeTruthy() - }) it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') }) From b6a199aa93efd3cbcb3eb7ffb14423ce6d4e5592 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 08:34:38 -0700 Subject: [PATCH 16/34] Revert "Initial addition of new loader for converting new URL() usage" This reverts commit ccefcb2dc2b2c6a13a68ad607e95788a5b82b52b. --- .../babel/plugins/image-import-transforms.ts | 69 ------------------- packages/next/build/babel/preset.ts | 1 - packages/next/client/image.tsx | 2 +- 3 files changed, 1 insertion(+), 71 deletions(-) delete mode 100644 packages/next/build/babel/plugins/image-import-transforms.ts diff --git a/packages/next/build/babel/plugins/image-import-transforms.ts b/packages/next/build/babel/plugins/image-import-transforms.ts deleted file mode 100644 index b8f19e9a7e928..0000000000000 --- a/packages/next/build/babel/plugins/image-import-transforms.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { PluginObj, types } from 'next/dist/compiled/babel/core' - -type UrlUsageData = { - url: string - metaName: string -} - -const getNewUrlSrc = (attribute: types.JSXAttribute) => { - return ( - attribute.name.name === 'src' && - (attribute.value as types.JSXExpressionContainer).expression && - (attribute.value as types.JSXExpressionContainer).expression.type === - 'NewExpression' && - (((attribute.value as types.JSXExpressionContainer) - .expression as types.NewExpression).callee as types.Identifier).name && - (((attribute.value as types.JSXExpressionContainer) - .expression as types.NewExpression).arguments[0] as types.StringLiteral) - .value - ) -} - -export default function ({ types: t }: { types: typeof types }): PluginObj { - let imageComponent: string | null = null - let newUrlUsages: UrlUsageData[] = [] - return { - visitor: { - Program: { - exit(path) { - const newImportStatements = newUrlUsages.map((urlUsage) => { - return t.importDeclaration( - [t.importDefaultSpecifier(t.identifier(urlUsage.metaName))], - t.stringLiteral(urlUsage.url) - ) - }) - path.unshiftContainer('body', newImportStatements) - }, - }, - ImportDeclaration(path) { - if (path.node.source.value === 'next/image') { - imageComponent = path.node.specifiers[0].local.name - } - }, - JSXOpeningElement(path) { - if ( - !imageComponent || - (path.node.name as types.JSXIdentifier).name !== imageComponent - ) { - return - } - const attributes = path.node.attributes - - for (let attribute of attributes) { - let url = getNewUrlSrc(attribute as types.JSXAttribute) - if (url) { - const metaName = 'meta' + newUrlUsages.length - newUrlUsages.push({ url, metaName }) - attributes.push( - t.jsxAttribute( - t.jsxIdentifier('meta'), - t.jsxExpressionContainer(t.identifier(metaName)) - ) - ) - break - } - } - }, - }, - } -} diff --git a/packages/next/build/babel/preset.ts b/packages/next/build/babel/preset.ts index 8919291c0df80..04a9aee246dd9 100644 --- a/packages/next/build/babel/preset.ts +++ b/packages/next/build/babel/preset.ts @@ -168,7 +168,6 @@ export default ( lib: true, }, ], - require('./plugins/image-import-transforms'), require('next/dist/compiled/babel/plugin-syntax-dynamic-import'), require('./plugins/react-loadable-plugin'), [ diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index af8af82dccf35..d1214050cab13 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -304,7 +304,7 @@ export default function Image({ if (!staticImageData.src) { throw new Error( `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( - staticImport + staticImageData )}` ) } From 809d86ee2ea4c8b32630d0f47c254c3ef7ec8569 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 13:07:15 -0700 Subject: [PATCH 17/34] Integrate static image and blurry placeholder --- .../webpack/loaders/next-image-loader.js | 25 ++++++++++++++++++- packages/next/client/image.tsx | 4 +++ .../image-component/default/pages/static.js | 15 +++++++++-- .../default/test/static.test.js | 11 ++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index af44ffa2bf967..5d5f27a9f78a8 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -1,7 +1,9 @@ import loaderUtils from 'next/dist/compiled/loader-utils' import sizeOf from 'image-size' +import { processBuffer } from '../../../next-server/server/lib/squoosh/main' +import fs from 'fs' -function nextImageLoader(content) { +async function nextImageLoader(content) { this.cacheable && this.cacheable(true) this.addDependency(this.resourcePath) @@ -13,6 +15,11 @@ function nextImageLoader(content) { opts ) + let extension = loaderUtils.interpolateName(this, '[ext]', opts) + if (extension === 'jpg') { + extension = 'jpeg' + } + if (interpolatedName.slice(0, 7) !== '/public') { const err = new Error( 'Static Image loader used with filepath not in the /public directory: ' + @@ -23,11 +30,27 @@ function nextImageLoader(content) { const src = interpolatedName.slice(7) const imageSize = sizeOf(this.resourcePath) + let dataURI + if (extension === 'jpeg' || extension === 'png') { + const fileBuffer = Buffer.from(fs.readFileSync(this.resourcePath)) + const resizedImage = await processBuffer( + fileBuffer, + [{ type: 'resize', width: 6 }], + extension, + 0 + ) + dataURI = `data:image/${extension};base64,${resizedImage.toString( + 'base64' + )}` + } + const stringifiedData = JSON.stringify({ src, height: imageSize.height, width: imageSize.width, + dataURI, }) + return `${'export default '} ${stringifiedData};` } diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index d1214050cab13..73015fb82e727 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -54,6 +54,7 @@ interface StaticImageData { src: string height: number width: number + dataURI?: string } interface StaticRequire { @@ -308,6 +309,9 @@ export default function Image({ )}` ) } + if (staticImageData.dataURI) { + blurDataURL = staticImageData.dataURI + } staticSrc = staticImageData.src if (!layout || layout !== 'fill') { height = staticImageData.height diff --git a/test/integration/image-component/default/pages/static.js b/test/integration/image-component/default/pages/static.js index e00219b11a111..4a38e76dd67bd 100644 --- a/test/integration/image-component/default/pages/static.js +++ b/test/integration/image-component/default/pages/static.js @@ -13,7 +13,12 @@ const Page = () => { return (

Static Image

- + { layout="fixed" /> {testFiles.map((f, i) => ( - + ))}
) diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js index 30d137a41b8b4..044c04fc2bbfc 100644 --- a/test/integration/image-component/default/test/static.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -41,6 +41,16 @@ const runTests = () => { await browser.elementById('basic-non-static').getAttribute('src') ).not.toContain('&s=1') }) + 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%;background-size:cover;background-image:url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wEEEAMgAyADIAMgA1IDIAOEA+gD6AOEBOIFRgSwBUYE4gc6BqQGDgYOBqQHOgrwB9AIZgfQCGYH0ArwEJoKWgwcCloKWgwcCloQmg6mEcYOdA16DnQRxg6mGl4UtBJcElwUtBpeHngZlhg4GZYeeCTqIQIhAiTqLnwsJC58PL48vlGkEQMgAyADIAMgA1IDIAOEA+gD6AOEBOIFRgSwBUYE4gc6BqQGDgYOBqQHOgrwB9AIZgfQCGYH0ArwEJoKWgwcCloKWgwcCloQmg6mEcYOdA16DnQRxg6mGl4UtBJcElwUtBpeHngZlhg4GZYeeCTqIQIhAiTqLnwsJC58PL48vlGk/8IAEQgABgAGAwEiAAIRAQMRAf/EABQAAQAAAAAAAAAAAAAAAAAAAAH/2gAIAQEAAAAAP//EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIQAAAAf//EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMQAAAAf//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAT8Af//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8Af//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8Af//Z")"` + ) + }) + 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%;background-size:cover;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAQAAABKxSfDAAAAOklEQVR42jXLsQ2AMADEQINYIT2ZgP2VfTLGmy+gObkxeImfhyXU1pSsrDoDPm53RfDOyKiE839y+gIFlSgsTCgClAAAAABJRU5ErkJggg==")"` + ) + }) } describe('Build Error Tests', () => { @@ -65,6 +75,7 @@ describe('Static Image Component Tests', () => { module.exports = { experimental: { staticImages: true, + enableBlurryPlaceholder: true, }, } ` From 2e183bdc615917405c64d96b576d0f983428cefc Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 13:11:29 -0700 Subject: [PATCH 18/34] Return from image loader after emitting error --- packages/next/build/webpack/loaders/next-image-loader.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index 5d5f27a9f78a8..c3e75dbdab1bf 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -26,6 +26,7 @@ async function nextImageLoader(content) { interpolatedName ) this.emitError(err) + return } const src = interpolatedName.slice(7) From 85d3d061a86dd70f29e4d6d45b92a44f2beae08e Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 13:33:35 -0700 Subject: [PATCH 19/34] Support additional file types for image loader --- packages/next/build/webpack-config.ts | 2 +- .../image-component/default/pages/static.js | 13 ++++++++++++- .../image-component/default/public/test.ico | Bin 0 -> 4286 bytes .../image-component/default/public/test.webp | Bin 0 -> 1018 bytes .../image-component/default/test/static.test.js | 3 +++ 5 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 test/integration/image-component/default/public/test.ico create mode 100644 test/integration/image-component/default/public/test.webp diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index ab9ba229155fd..59160957a414b 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1001,7 +1001,7 @@ export default async function getBaseWebpackConfig( ...(config.experimental.staticImages ? [ { - test: /\.(png|svg|jpg|jpeg|gif)$/i, + test: /\.(png|svg|jpg|jpeg|gif|webp|ico|bmp)$/i, loader: 'next-image-loader', }, ] diff --git a/test/integration/image-component/default/pages/static.js b/test/integration/image-component/default/pages/static.js index 4a38e76dd67bd..f49a7130107cb 100644 --- a/test/integration/image-component/default/pages/static.js +++ b/test/integration/image-component/default/pages/static.js @@ -6,8 +6,19 @@ import testJPG from '../public/test.jpg' import testPNG from '../public/test.png' 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 testWEBP from '../public/test.webp' -const testFiles = [testJPG, testPNG, testSVG, testGIF] +const testFiles = [ + testJPG, + testPNG, + testSVG, + testGIF, + testBMP, + testICO, + testWEBP, +] const Page = () => { return ( diff --git a/test/integration/image-component/default/public/test.ico b/test/integration/image-component/default/public/test.ico new file mode 100644 index 0000000000000000000000000000000000000000..55cce0b4a854789496f8e303aa4937d87eed7d32 GIT binary patch literal 4286 zcmeHKI|_q95FNo%AyvReQc3U>dJYfbNz_uyl!B-55FSBWJ16@r(j>a8=2y(f$nxHs z55ue=k)@u?h47}^iRes3SLF(#NICG?6!*edVd63a*#X%B*?|{3U|H7Yx$KEq`VI#{ zz(rATRaIP;<*GviEn=4PZN**J<)&%a)SyMo);eqYIF7mRduuIu_yA7@~u@$b;#jt#>wdlW@qt&RS1$MQU%{Zsc%Hz(is8Lra_ SFaH;^1F{2ux&x}M-`O2o>eJW& literal 0 HcmV?d00001 diff --git a/test/integration/image-component/default/public/test.webp b/test/integration/image-component/default/public/test.webp new file mode 100644 index 0000000000000000000000000000000000000000..4b306cb0898cc93e83dd6f72566e13cbd1339eaa GIT binary patch literal 1018 zcmWIYbaVT}%)k)t>J$(bV4?5~$lhSgFqctl0^gPIDuB8qHv48H-EEbk|FBuIt(=eGzI!$@2M@ z#TTkpe-pQ_(pI`{*bt+CqK4g>WuD^-8`F(nA0CC+|Bv@@pq)xt!UWH|e=J2I&PYA^ zWlqk)0|9oTHfu`3u4+!vT_4xYrvJXmxxu=z`*eD{~o~@4h9AWfq9;% zqR$+A*q{}?M)}6!)9;$Um~P0+FB3LdlX7i)VN-;n!!5QNkGLvHafX9Y89~=x2}Fh~ z6{=Xw=Xf8owQpHQQhfe*DWBsR$IZ_)v2DNh4;YT~5_h_!EDZl17V~0$^#W^&%R;M9 z1TEjSC+p$CeWvFQFka)8vCV&;F*&+u0)t$j-NbM5|Eshts#iT?RJqI9Y@6$P?sJE; z(L-rLjkkxn89m~?Lr5i{_oxx zSN0*UK=A6}>)z2FvXA5!`;<=WJryi<{b@Bvy@XxL&U~+rMl7KV^LSQ#f56oEGG)cK z=rpw;j*oejQy>@ zO`}}>1>=?-D{WT%kUkpAsF(X!&bNaVHSjask0+H4Wp zPLZ)O`L|h_E7aw-7d((IxEm7R?EL(E9LSfq9oZdPx)1Qg`#-(@lK0TJ8-elG*X7c( z`yam$?FnSP{2}Dw-$B|+seK+ P+udJbdSnVPy+8l}7RKRp literal 0 HcmV?d00001 diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js index 044c04fc2bbfc..b5b1b44c73704 100644 --- a/test/integration/image-component/default/test/static.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -27,6 +27,9 @@ const runTests = () => { expect(await browser.elementById('format-test-1')).toBeTruthy() expect(await browser.elementById('format-test-2')).toBeTruthy() expect(await browser.elementById('format-test-3')).toBeTruthy() + expect(await browser.elementById('format-test-4')).toBeTruthy() + expect(await browser.elementById('format-test-5')).toBeTruthy() + expect(await browser.elementById('format-test-6')).toBeTruthy() }) it('Should automatically provide an image height and width', async () => { expect(html).toContain('width:400px;height:300px') From f9372334537b8dc1ccffb625a2cc98aeb6f96f13 Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 13:36:44 -0700 Subject: [PATCH 20/34] standardize variable names for isStatic image --- packages/next/next-server/server/image-optimizer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 5b85d2ded7366..848ad10d99955 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -117,7 +117,7 @@ export async function imageOptimizer( return { finished: true } } - const isStaticImage = !!s + const isStatic = !!s const width = parseInt(w, 10) @@ -269,7 +269,7 @@ export async function imageOptimizer( ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer) if (vector || animate) { await writeToCacheDir(hashDir, upstreamType, expireAt, upstreamBuffer) - sendResponse(req, res, upstreamType, upstreamBuffer, isStaticImage) + sendResponse(req, res, upstreamType, upstreamBuffer, isStatic) return { finished: true } } @@ -341,12 +341,12 @@ export async function imageOptimizer( if (optimizedBuffer) { await writeToCacheDir(hashDir, contentType, expireAt, optimizedBuffer) - sendResponse(req, res, contentType, optimizedBuffer, isStaticImage) + sendResponse(req, res, contentType, optimizedBuffer, isStatic) } else { throw new Error('Unable to optimize buffer') } } catch (error) { - sendResponse(req, res, upstreamType, upstreamBuffer, isStaticImage) + sendResponse(req, res, upstreamType, upstreamBuffer, isStatic) } return { finished: true } From 9659f46ef6802833df12dac5ac21b22fe97edefb Mon Sep 17 00:00:00 2001 From: atcastle Date: Mon, 24 May 2021 13:41:19 -0700 Subject: [PATCH 21/34] rename staticImages experimental config to enableStaticImages --- packages/next/build/webpack-config.ts | 2 +- packages/next/next-server/server/config-shared.ts | 4 ++-- test/integration/image-component/default/test/index.test.js | 6 +++--- .../integration/image-component/default/test/static.test.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 59160957a414b..d6f3f3086808f 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -998,7 +998,7 @@ export default async function getBaseWebpackConfig( ] : defaultLoaders.babel, }, - ...(config.experimental.staticImages + ...(config.experimental.enableStaticImages ? [ { test: /\.(png|svg|jpg|jpeg|gif|webp|ico|bmp)$/i, diff --git a/packages/next/next-server/server/config-shared.ts b/packages/next/next-server/server/config-shared.ts index ad1ed3d370750..0274481fcf8a1 100644 --- a/packages/next/next-server/server/config-shared.ts +++ b/packages/next/next-server/server/config-shared.ts @@ -45,7 +45,7 @@ export type NextConfig = { [key: string]: any } & { workerThreads?: boolean pageEnv?: boolean optimizeImages?: boolean - staticImages?: boolean + enableStaticImages?: boolean optimizeCss?: boolean scrollRestoration?: boolean scriptLoader?: boolean @@ -111,7 +111,7 @@ export const defaultConfig: NextConfig = { workerThreads: false, pageEnv: false, optimizeImages: false, - staticImages: false, + enableStaticImages: false, optimizeCss: false, scrollRestoration: false, scriptLoader: false, diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js index 09c16a313bc9f..f4ba8b4ec7bee 100644 --- a/test/integration/image-component/default/test/index.test.js +++ b/test/integration/image-component/default/test/index.test.js @@ -567,7 +567,7 @@ describe('Image Component Tests', () => { ` module.exports = { experimental: { - staticImages: true + enableStaticImages: true }, } ` @@ -590,7 +590,7 @@ describe('Image Component Tests', () => { ` module.exports = { experimental: { - staticImages: true + enableStaticImages: true }, } ` @@ -616,7 +616,7 @@ describe('Image Component Tests', () => { target: 'serverless', experimental: { enableBlurryPlaceholder: true, - staticImages: true + enableStaticImages: true }, } ` diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js index b5b1b44c73704..2eaf5f2b0560e 100644 --- a/test/integration/image-component/default/test/static.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -77,7 +77,7 @@ describe('Static Image Component Tests', () => { ` module.exports = { experimental: { - staticImages: true, + enableStaticImages: true, enableBlurryPlaceholder: true, }, } From c65e4160044aecef775dc9cb287f472c641cc484 Mon Sep 17 00:00:00 2001 From: atcastle Date: Wed, 26 May 2021 12:22:43 -0700 Subject: [PATCH 22/34] Only set height and width for static images if they're not provided --- packages/next/client/image.tsx | 6 +++--- .../image-component/default/pages/static.js | 16 ++++++---------- .../image-component/default/test/static.test.js | 4 ++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 73015fb82e727..e29fd88ce6db8 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -314,9 +314,9 @@ export default function Image({ } staticSrc = staticImageData.src if (!layout || layout !== 'fill') { - height = staticImageData.height - width = staticImageData.width - if (!height || !width) { + height = height || staticImageData.height + width = width || staticImageData.width + if (!staticImageData.height || !staticImageData.width) { throw new Error( `An object should only be passed to the image component src parameter if it comes from a static image import. It must include height and width. Received ${JSON.stringify( staticImageData diff --git a/test/integration/image-component/default/pages/static.js b/test/integration/image-component/default/pages/static.js index f49a7130107cb..e6b5cecf97ba0 100644 --- a/test/integration/image-component/default/pages/static.js +++ b/test/integration/image-component/default/pages/static.js @@ -31,25 +31,21 @@ const Page = () => { placeholder="blur" /> + {testFiles.map((f, i) => ( - + ))}
) diff --git a/test/integration/image-component/default/test/static.test.js b/test/integration/image-component/default/test/static.test.js index 2eaf5f2b0560e..1c4e5e1a0b6d7 100644 --- a/test/integration/image-component/default/test/static.test.js +++ b/test/integration/image-component/default/test/static.test.js @@ -34,6 +34,10 @@ const runTests = () => { 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 append "&s=1" to URLs of static images', async () => { expect( await browser.elementById('basic-static').getAttribute('src') From 357523afe4290dee8d3d93bf25b51f37316a6099 Mon Sep 17 00:00:00 2001 From: atcastle Date: Wed, 26 May 2021 12:55:34 -0700 Subject: [PATCH 23/34] Refactor static image argument detection to use type guards --- packages/next/client/image.tsx | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index e29fd88ce6db8..4a56d9f151fd9 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -63,6 +63,26 @@ interface StaticRequire { type StaticImport = StaticRequire | StaticImageData +function isStaticRequire( + src: StaticRequire | StaticImageData +): src is StaticRequire { + return (src as StaticRequire).default !== undefined +} + +function isStaticImageData( + src: StaticRequire | StaticImageData +): src is StaticImageData { + return (src as StaticImageData).src !== undefined +} + +function isStaticImport(src: string | StaticImport): src is StaticImport { + return ( + typeof src === 'object' && + (isStaticRequire(src as StaticImport) || + isStaticImageData(src as StaticImport)) + ) +} + export type ImageProps = Omit< JSX.IntrinsicElements['img'], 'src' | 'srcSet' | 'ref' | 'width' | 'height' | 'loading' | 'style' @@ -294,14 +314,9 @@ export default function Image({ } const isStatic = typeof src === 'object' let staticSrc = '' - if (isStatic) { - let staticImport = src as StaticImport - let staticImageData: StaticImageData - if ((staticImport as StaticRequire).default) { - staticImageData = (staticImport as StaticRequire).default - } else { - staticImageData = staticImport as StaticImageData - } + if (isStaticImport(src)) { + const staticImageData = isStaticRequire(src) ? src.default : src + if (!staticImageData.src) { throw new Error( `An object should only be passed to the image component src parameter if it comes from a static image import. It must include src. Received ${JSON.stringify( From 0ed6844c19d8ec2b8a3e5005927422b9e463f6a9 Mon Sep 17 00:00:00 2001 From: atcastle Date: Wed, 26 May 2021 15:06:02 -0700 Subject: [PATCH 24/34] Fix static image test label --- test/integration/image-optimizer/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 07e8d19be2a2c..996f5b1ae0d77 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -217,7 +217,7 @@ function runTests({ w, isDev, domains }) { ) }) - it('should fail when s is present and not "true"', async () => { + it('should fail when s is present and not "1"', async () => { const query = { url: '/test.png', w, q: 100, s: 'foo' } const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) expect(res.status).toBe(400) From 6b8f5fb7846d89129d69cb404929de41ffb5c637 Mon Sep 17 00:00:00 2001 From: atcastle Date: Wed, 26 May 2021 15:12:27 -0700 Subject: [PATCH 25/34] Remove unneccessary lines from next-image-loader --- packages/next/build/webpack/loaders/next-image-loader.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index c3e75dbdab1bf..30a3724940465 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -4,9 +4,6 @@ import { processBuffer } from '../../../next-server/server/lib/squoosh/main' import fs from 'fs' async function nextImageLoader(content) { - this.cacheable && this.cacheable(true) - this.addDependency(this.resourcePath) - const context = this.rootContext const opts = { context, content } const interpolatedName = loaderUtils.interpolateName( From a23b5f0576935054d6c43e6c7447937b3bb09bc2 Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 27 May 2021 17:29:55 -0700 Subject: [PATCH 26/34] In image-loader, shrink images based on largest dimension --- .../webpack/loaders/next-image-loader.js | 9 +++++++- .../next-server/server/lib/squoosh/impl.ts | 8 ++++++- .../next-server/server/lib/squoosh/main.ts | 23 +++++++++++++++---- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index 30a3724940465..507f5ce111fce 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -3,6 +3,8 @@ import sizeOf from 'image-size' import { processBuffer } from '../../../next-server/server/lib/squoosh/main' import fs from 'fs' +const PLACEHOLDER_SIZE = 6 + async function nextImageLoader(content) { const context = this.rootContext const opts = { context, content } @@ -31,9 +33,14 @@ async function nextImageLoader(content) { let dataURI if (extension === 'jpeg' || extension === 'png') { const fileBuffer = Buffer.from(fs.readFileSync(this.resourcePath)) + // Shrink the image's largest dimension to 6 pixels + const resizeOperationOpts = + imageSize.width >= imageSize.height + ? { type: 'resize', width: PLACEHOLDER_SIZE } + : { type: 'resize', height: PLACEHOLDER_SIZE } const resizedImage = await processBuffer( fileBuffer, - [{ type: 'resize', width: 6 }], + [resizeOperationOpts], extension, 0 ) diff --git a/packages/next/next-server/server/lib/squoosh/impl.ts b/packages/next/next-server/server/lib/squoosh/impl.ts index a814249ea89d3..b9efc9859114f 100644 --- a/packages/next/next-server/server/lib/squoosh/impl.ts +++ b/packages/next/next-server/server/lib/squoosh/impl.ts @@ -29,7 +29,12 @@ export async function rotate( return await m(image.data, image.width, image.height, { numRotations }) } -export async function resize(image: ImageData, width: number) { +type ResizeOpts = { image: ImageData } & ( + | { width: number; height?: never } + | { height: number; width?: never } +) + +export async function resize({ image, width, height }: ResizeOpts) { image = ImageData.from(image) const p = preprocessors['resize'] @@ -37,6 +42,7 @@ export async function resize(image: ImageData, width: number) { return await m(image.data, image.width, image.height, { ...p.defaultOptions, width, + height, }) } diff --git a/packages/next/next-server/server/lib/squoosh/main.ts b/packages/next/next-server/server/lib/squoosh/main.ts index 8fbcb89212a47..4b885764481ab 100644 --- a/packages/next/next-server/server/lib/squoosh/main.ts +++ b/packages/next/next-server/server/lib/squoosh/main.ts @@ -9,8 +9,7 @@ type RotateOperation = { } type ResizeOperation = { type: 'resize' - width: number -} +} & ({ width: number; height?: never } | { height: number; width?: never }) export type Operation = RotateOperation | ResizeOperation export type Encoding = 'jpeg' | 'png' | 'webp' @@ -38,8 +37,24 @@ export async function processBuffer( if (operation.type === 'rotate') { imageData = await worker.rotate(imageData, operation.numRotations) } else if (operation.type === 'resize') { - if (imageData.width && imageData.width > operation.width) { - imageData = await worker.resize(imageData, operation.width) + if ( + operation.width && + imageData.width && + imageData.width > operation.width + ) { + imageData = await worker.resize({ + image: imageData, + width: operation.width, + }) + } else if ( + operation.height && + imageData.height && + imageData.height > operation.height + ) { + imageData = await worker.resize({ + image: imageData, + height: operation.height, + }) } } } From 6cb22b8cd8852a0c542f56c4251bbd637e200311 Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 27 May 2021 17:34:57 -0700 Subject: [PATCH 27/34] Rename dataURI to placeholder in image loader --- packages/next/build/webpack/loaders/next-image-loader.js | 6 +++--- packages/next/client/image.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index 507f5ce111fce..3dd25d8d1ef37 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -30,7 +30,7 @@ async function nextImageLoader(content) { const src = interpolatedName.slice(7) const imageSize = sizeOf(this.resourcePath) - let dataURI + let placeholder if (extension === 'jpeg' || extension === 'png') { const fileBuffer = Buffer.from(fs.readFileSync(this.resourcePath)) // Shrink the image's largest dimension to 6 pixels @@ -44,7 +44,7 @@ async function nextImageLoader(content) { extension, 0 ) - dataURI = `data:image/${extension};base64,${resizedImage.toString( + placeholder = `data:image/${extension};base64,${resizedImage.toString( 'base64' )}` } @@ -53,7 +53,7 @@ async function nextImageLoader(content) { src, height: imageSize.height, width: imageSize.width, - dataURI, + placeholder, }) return `${'export default '} ${stringifiedData};` diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx index 4a56d9f151fd9..d8319298c34e8 100644 --- a/packages/next/client/image.tsx +++ b/packages/next/client/image.tsx @@ -54,7 +54,7 @@ interface StaticImageData { src: string height: number width: number - dataURI?: string + placeholder?: string } interface StaticRequire { @@ -324,8 +324,8 @@ export default function Image({ )}` ) } - if (staticImageData.dataURI) { - blurDataURL = staticImageData.dataURI + if (staticImageData.placeholder) { + blurDataURL = staticImageData.placeholder } staticSrc = staticImageData.src if (!layout || layout !== 'fill') { From 88d28f220f3ab0b12f858bae08ae23eedd5cb9e2 Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 27 May 2021 17:35:53 -0700 Subject: [PATCH 28/34] Exclude URL dependencies from using image loader --- packages/next/build/webpack-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index d6f3f3086808f..e0632cd502930 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1003,6 +1003,7 @@ export default async function getBaseWebpackConfig( { test: /\.(png|svg|jpg|jpeg|gif|webp|ico|bmp)$/i, loader: 'next-image-loader', + dependency: { not: 'url' }, }, ] : []), From cb63fbbfd68f669cb1a8a54b50c936aa02996d01 Mon Sep 17 00:00:00 2001 From: atcastle Date: Fri, 28 May 2021 15:46:53 -0700 Subject: [PATCH 29/34] Allow image's to be imported from locations other than /public --- packages/next/build/webpack-config.ts | 3 ++- .../webpack/loaders/next-image-loader.js | 20 ++++++------------ .../default/components/TallImage.js | 20 ++++++++++++++++++ .../default/components/tall.png | Bin 0 -> 6391 bytes .../image-component/default/pages/static.js | 2 ++ 5 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 test/integration/image-component/default/components/TallImage.js create mode 100644 test/integration/image-component/default/components/tall.png diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index e0632cd502930..4d623c04929bb 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -939,6 +939,7 @@ export default async function getBaseWebpackConfig( crossOriginLoading: crossOrigin, futureEmitAssets: !dev, webassemblyModuleFilename: 'static/wasm/[modulehash].wasm', + assetModuleFilename: 'static/[name].[hash].[ext]', }, performance: false, resolve: resolveConfig, @@ -1003,7 +1004,7 @@ export default async function getBaseWebpackConfig( { test: /\.(png|svg|jpg|jpeg|gif|webp|ico|bmp)$/i, loader: 'next-image-loader', - dependency: { not: 'url' }, + dependency: { not: ['url'] }, }, ] : []), diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index 3dd25d8d1ef37..c01683b8434ec 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -10,7 +10,7 @@ async function nextImageLoader(content) { const opts = { context, content } const interpolatedName = loaderUtils.interpolateName( this, - '/[path][name].[ext]', + '/static/image/[path][name].[hash].[ext]', opts ) @@ -19,20 +19,10 @@ async function nextImageLoader(content) { extension = 'jpeg' } - if (interpolatedName.slice(0, 7) !== '/public') { - const err = new Error( - 'Static Image loader used with filepath not in the /public directory: ' + - interpolatedName - ) - this.emitError(err) - return - } - - const src = interpolatedName.slice(7) const imageSize = sizeOf(this.resourcePath) let placeholder + const fileBuffer = Buffer.from(fs.readFileSync(this.resourcePath)) if (extension === 'jpeg' || extension === 'png') { - const fileBuffer = Buffer.from(fs.readFileSync(this.resourcePath)) // Shrink the image's largest dimension to 6 pixels const resizeOperationOpts = imageSize.width >= imageSize.height @@ -50,13 +40,15 @@ async function nextImageLoader(content) { } const stringifiedData = JSON.stringify({ - src, + src: '/_next' + interpolatedName, height: imageSize.height, width: imageSize.width, placeholder, }) + this.emitFile(interpolatedName, fileBuffer, null) + return `${'export default '} ${stringifiedData};` } - +nextImageLoader.raw = true export default nextImageLoader diff --git a/test/integration/image-component/default/components/TallImage.js b/test/integration/image-component/default/components/TallImage.js new file mode 100644 index 0000000000000..c0fbbcfe6d63c --- /dev/null +++ b/test/integration/image-component/default/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/default/components/tall.png b/test/integration/image-component/default/components/tall.png new file mode 100644 index 0000000000000000000000000000000000000000..a792dda6c172ffb6254d0d82e3a9a8a7cfe4d3e3 GIT binary patch literal 6391 zcmeHL`9GBF`=7xPQp72dW6wHcP_{9K(2?vZYh!F^e!qXh_w_o@^TYkVulu^M<^8(u>waEOnv;X|J`oua z006Mh=9JYL-dF+v0B;Bh@=$u#=`H|3V1J0ErBkS-wPpClaEwcYuRqEfg+^gQ{Lffh z002feZ=OR0%bxocUeG|*Qt3?@EGx_2&(*(EbMZ|7mEk!qiW)Ww%DGr`;#SxESW$@T zv8k;;LNwsNo)CZaa3W`7$Lp1v;)N&dbnyKer~3ja>KlDOt?#{)5@t0VqtbGohc?ki zH)jeL6KBV<3*&olJqLLa$B*)XWS)Yj2PUbVGi_~9+^dAXeF=SFt;}4_Tvgzasn;)u z*^3Gt<3nrp>pMZLr%u+u)Um4{D~!68CQ$(H!FCEFS>Occjqs4)hH#t7#X^NiiPlrdB?^OdTQ3!P5~Th(gWEQex@T5-%~N0xc4;INrvI=~Ly3N}%iu}AgYrOA3MZ-V zlMGy0Tog zeTZf%{ia*6LFf0>jps9m0{Idaz(FCT_}osqZ75Y`ZL&4)6%8jClvB6KG`h)``lm~TfAU4@48?s6{Uv#7u zA1Gw2b-4s&1oT5s@7M1nDq#aw8u|xgfy!sdhUtyzb1^46xIqDTCu$h&r^$xm9~mDR zq20MdXm4l&+AW>-yDORY0s9rF5+rj0jC(UYScGBhyhhFrRU>B&Jw>^D68V!@Bd+!O ztm8edA_*_Tz1pl-OdSH?rQmZHeW?%bd+59V)GH$|x zU$?#vuyvd}c^=mj^Ll zDfIo586ibAwfWy}w03=&@)N1j(>x?P-BO~?dND_n?JwJ3n-@F{sE*e&dL3@V-If}O zEk$9;f<{|vChMMc#$po{{C%8Yxn~Ecp}eQ z^}?f%jsrdtn!I;7+i?ji{DVxu^O7C@oul}@?fxI=yar)~a<>Vzw+CqQun>R`C;{N- zAt3JsTn_;V{D}boRo(~y@RjiWBPl82|1SpK*j)ra3s&Q8^awfYj&rxSgZhVuY5N9* z`=PY)VG+9u02m(1gJCF~uL3^oA{qPZeCKsgk?BlY(V92Bl!2 zt)s1@1Q$_IP=H|q0-t=EDKAyH z2n_NMZEz83@6USx0EtB#s}pDOKz5OR_LP?lOP9otm{b9C@;$Xrg7hza=tgaxR`qO6Pi(wmzTFbAhuV)Q;6{4omP>>L=vA+ zsz{}Pc=bWEB4vgBH4^46u3NawWTU>*0HD|MfHB{sxfW31V!Pw4P%8hwivQB{XHjV5 zCOC^PllsNz2?FDGkfbDJsxET7Q$9)7VT-odd_>axvxlvDqJ(H!&-!?J-RGD*vzR8l z(FQVcbDE>U`E83cOHLYRb@F{P4yQJMdQxzNFQz;D<<|G~9Hz4c+wyt4960fFL|^Xb z!qoues454%lIpQ^=fzWS&iU)AX4^jo)a=c6wpQ|zcDC222eIVift@ydttd*G#m_?h zPfi^LkVtvSY?zxgsrj&Sv#JAZrnTz2U#;}*w?(rV$wPo%DzYbz$T*Yz3l{F2T0Bpy zv&(MU@P<5Hg^umSlBWpE?NZ|b30DC*x3rSWBF@WB8Aq#;f{i^+dzHNQrl~i_S2%( zM)<0%WOe~%!v5y9oIzBciytDpm;L@4rN2_i!#ZG1A z>zY$53->dKF@%y5dk~q5psk@?Ws`n1MA@rG4YN zbQo{dM0t39q1}*O`SFL0MYPsjVgm+hH>W59hAe1Xw0k=U&nlqM`QEJ zn$R#m5M?5ic@5HPmy?oLAI?m~q)c+^9gE$cw^A=uYS0N+jJ8zTB*heO!%%)Gr`@h}{SWyheA4*(B8*5? zM3J)2XG!}G>-Vgcrdjv6t_PZReusAWTVR&LWWP=x=|SbGcQmlo-@aqJJky{zHp*j! ziuD-udS+t$VSd<1kyx($p44ftmAfmQP{H5mb99~71>8L`!=07+^7${K)2r|7V!lOX zN^>GjC==%E2@(;Smo|pUxxKGtkBmQyJPo~?o-lV8cQJ*Vft}P$=z(y3J9l=hG$y$> z!aCt1rn?*Cc#3k&;;_N4hU7 z+nYVDY%XDu4dRtY6#)VDXvDr^vT@f9W8r6$S%DYAE?JS*I zdX})gIs{o9Z(>1f%2#PD(Yg7c{6oD`;~bMA_@UnlvK(2rDHGasonrji@hVeI*G937 zgbW?UPi%46SdY9}L?7uhajmjvV+fhll(A%{%hu*cYjD~#VlKoldPL+&N65U}pVqm4 z)|T2QMc~o>WDXeu>EiB3?KN4qhGk>9>iKgV$G*2Odo^+jlD`_jqrvzQfp;sstpAT*M=5 zndY;-|NUfaQ*^`A->OFKd0(umQlQ=1W5$+%oG(1l zvF$ZdOuinp9=}=BQ+BLG@6xeiNQOWGvZm5TD$6j?jGVW|*4T@th(0KYWF84&hcWk# zW5R2%8o;YuSVeZH>WqbXjY9q1`HqdzL-pX|EYo79L$}_Dqz(~5@ckVTZn_uEBPp%3 z>LvHZSH{ow_{R9U1SYF$>tI1;LPiocM|_OHDX3g%z{le&)qJ?!%*w9v(CSB`4`VjQ z;cN`!$SY|lCG_fmUO{(qyr$`>vNxfUe%vdNnx>aQ*N!}7^sP;~IHvGuc%%h|q=?rv zmR;CBpRo!JOzE$C+hVzVXeTv9(sJoXFPz&vWSJS>Vx6O1T;&l=u3I&Ud^Y@!k|t`j z@7SI)V(;P>_)_BFv;3>nlAnw0ihtSwqTbLGMdfuKBA(6n@b?JR zw~W~BgbL|n)-AG4AMy}S*;+|%x&ob)bl;G-7zHt&^gl#xs%T_u=HR!M?p<-3Js^&9p z{P(V<*;=+APMMzS)uXJ|#k08AD$9e+$F{x|86pdXr4wfZ7UE^8#UJyj>nu+k5moGY z&H2;xyBM0#4dB@IA~%Nc-OBa??7~kpsXVYH)Mdx`j8OpF~CpLe9;)t_^lOWLWzID+SlhGjKf+hLvWNIiL9KG$2a zueQ(8+GFOkbl=Ox_ZpmWOEngINFD#d;jE5jz?fNYrzWfAo z&an}1D(_6xkaR*xZCh*fCJKF2wnJ6v@@pm3dLh2*+b3DRxT1r`7&O{HD zlJwpYt8QmEiRbl3MTdCJP}A{>`-t*He=l-V%W%9YJ`Z~Phl-UUS@RUdyrtfQizZbN zluvFBFT$onX+l)}KwhG!YA#T#eB2-Lc)PgrOHQio*mGn43zPSd4Y83wSN%Wu zOQcf29OZp-{Zg2R@ay!yKU=|0{@_X6Ixt{wSxq4)Fu!94sgaq!k zJ5o%Xh7sfD37>^4a!lThlrHV$!ZMoAyp%&df7*O9AK{bcbMW&dYtb9KdHvy3^IeN$Meh#rsZOYrtu|14botLj>l%E>QYg7n zwkTbgG`;BD;vOCOjnH;U94pJW_J-w>)P_5J+5c1f&Dy|AEv5o60h5kLl%9f+$z?8k zq#lhu&p;9gm-h*Q6vQRHKy<=P+I7Fv&>hfRN}l<|L15N>BEzwR(sN|HokksNyoYR& z;j { layout="fixed" placeholder="blur" /> + Date: Fri, 28 May 2021 15:57:31 -0700 Subject: [PATCH 30/34] Removed unneeded AssetModuleFilename property --- packages/next/build/webpack-config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 4d623c04929bb..f9bfb3516ed41 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -939,7 +939,6 @@ export default async function getBaseWebpackConfig( crossOriginLoading: crossOrigin, futureEmitAssets: !dev, webassemblyModuleFilename: 'static/wasm/[modulehash].wasm', - assetModuleFilename: 'static/[name].[hash].[ext]', }, performance: false, resolve: resolveConfig, From 09b07993aa2f711d9db6adec15085f6ef1833ff7 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 31 May 2021 18:13:05 +0200 Subject: [PATCH 31/34] Reuse buffer from webpack --- .../next/build/webpack/loaders/next-image-loader.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js index c01683b8434ec..7299c8afbb3e8 100644 --- a/packages/next/build/webpack/loaders/next-image-loader.js +++ b/packages/next/build/webpack/loaders/next-image-loader.js @@ -1,7 +1,6 @@ import loaderUtils from 'next/dist/compiled/loader-utils' import sizeOf from 'image-size' import { processBuffer } from '../../../next-server/server/lib/squoosh/main' -import fs from 'fs' const PLACEHOLDER_SIZE = 6 @@ -19,9 +18,8 @@ async function nextImageLoader(content) { extension = 'jpeg' } - const imageSize = sizeOf(this.resourcePath) + const imageSize = sizeOf(content) let placeholder - const fileBuffer = Buffer.from(fs.readFileSync(this.resourcePath)) if (extension === 'jpeg' || extension === 'png') { // Shrink the image's largest dimension to 6 pixels const resizeOperationOpts = @@ -29,7 +27,7 @@ async function nextImageLoader(content) { ? { type: 'resize', width: PLACEHOLDER_SIZE } : { type: 'resize', height: PLACEHOLDER_SIZE } const resizedImage = await processBuffer( - fileBuffer, + content, [resizeOperationOpts], extension, 0 @@ -46,9 +44,9 @@ async function nextImageLoader(content) { placeholder, }) - this.emitFile(interpolatedName, fileBuffer, null) + this.emitFile(interpolatedName, content, null) return `${'export default '} ${stringifiedData};` } -nextImageLoader.raw = true +export const raw = true export default nextImageLoader From 4e387d7ed6b2ed6b51c5178fe1a6ffe6761f0aed Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Mon, 31 May 2021 20:03:52 +0200 Subject: [PATCH 32/34] Update packages/next/next-server/server/image-optimizer.ts Co-authored-by: Steven --- packages/next/next-server/server/image-optimizer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 848ad10d99955..06eb3ce378b01 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -380,7 +380,7 @@ function sendResponse( const etag = getHash([buffer]) res.setHeader( 'Cache-Control', - isStatic ? 'immutable' : 'public, max-age=0, must-revalidate' + isStatic ? 'public, immutable, max-age=315360000' : 'public, max-age=0, must-revalidate' ) if (sendEtagResponse(req, res, etag)) { return From 9932cd8d3612c22c09f857d05b3bb9609ec3e3cd Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 3 Jun 2021 09:53:32 -0700 Subject: [PATCH 33/34] Update image-optimizer tests to reflect static image changes --- test/integration/image-optimizer/test/index.test.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 996f5b1ae0d77..db8bbe1fba44a 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -221,7 +221,7 @@ function runTests({ w, isDev, domains }) { const query = { url: '/test.png', w, q: 100, s: 'foo' } const res = await fetchViaHTTP(appPort, '/_next/image', query, {}) expect(res.status).toBe(400) - expect(await res.text()).toBe(`"s" parameter must be "true" or omitted`) + expect(await res.text()).toBe(`"s" parameter must be "1" or omitted`) }) it('should fail when domain is not defined in next.config.js', async () => { @@ -511,11 +511,13 @@ function runTests({ w, isDev, domains }) { }) it('should set cache-control to immutable for static images', async () => { - const query = { url: '/test.jpg', w, q: 100, s: 'true' } + const query = { url: '/test.jpg', w, q: 100, s: '1' } const opts = { headers: { accept: 'image/webp' } } const res = await fetchViaHTTP(appPort, '/_next/image', query, opts) expect(res.status).toBe(200) - expect(res.headers.get('cache-control')).toBe('immutable') + expect(res.headers.get('cache-control')).toBe( + 'public, immutable, max-age=315360000' + ) }) it("should error if the resource isn't a valid image", async () => { From cfe7b3810638625299e18894b415f242d4b10ef7 Mon Sep 17 00:00:00 2001 From: atcastle Date: Thu, 3 Jun 2021 10:36:15 -0700 Subject: [PATCH 34/34] Run prettier on image-optimizer.ts --- packages/next/next-server/server/image-optimizer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 06eb3ce378b01..9cb514b8ed3f0 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -380,7 +380,9 @@ function sendResponse( const etag = getHash([buffer]) res.setHeader( 'Cache-Control', - isStatic ? 'public, immutable, max-age=315360000' : 'public, max-age=0, must-revalidate' + isStatic + ? 'public, immutable, max-age=315360000' + : 'public, max-age=0, must-revalidate' ) if (sendEtagResponse(req, res, etag)) { return