Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure animated assets aren't de-animated by optimizer #17974

Merged
merged 2 commits into from Oct 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -82,6 +82,7 @@
"get-port": "5.1.1",
"glob": "7.1.6",
"gzip-size": "5.1.1",
"is-animated": "2.0.0",
"isomorphic-unfetch": "3.0.0",
"jest-circus": "26.0.1",
"jest-cli": "24.9.0",
Expand Down
1 change: 1 addition & 0 deletions packages/next/compiled/is-animated/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/next/compiled/is-animated/package.json
@@ -0,0 +1 @@
{"name":"is-animated","main":"index.js","author":"Józef Sokołowski <j.k.sokolowski@gmail.com>","license":"MIT"}
16 changes: 16 additions & 0 deletions packages/next/next-server/server/image-optimizer.ts
Expand Up @@ -7,14 +7,18 @@ import { createHash } from 'crypto'
import Server from './next-server'
import { getContentType, getExtension } from './serve-static'
import { fileExists } from '../../lib/file-exists'
// @ts-ignore no types for is-animated
import isAnimated from 'next/dist/compiled/is-animated'

let sharp: typeof import('sharp')
//const AVIF = 'image/avif'
const WEBP = 'image/webp'
const PNG = 'image/png'
const JPEG = 'image/jpeg'
const GIF = 'image/gif'
const MIME_TYPES = [/* AVIF, */ WEBP, PNG, JPEG]
const CACHE_VERSION = 1
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]

export async function imageOptimizer(
server: Server,
Expand Down Expand Up @@ -146,6 +150,18 @@ export async function imageOptimizer(
const expireAt = maxAge * 1000 + now
let contentType: string

if (
upstreamType &&
ANIMATABLE_TYPES.includes(upstreamType) &&
isAnimated(upstreamBuffer)
) {
if (upstreamType) {
res.setHeader('Content-Type', upstreamType)
}
res.end(upstreamBuffer)
return { finished: true }
}

if (mimeType) {
contentType = mimeType
} else if (upstreamType?.startsWith('image/') && getExtension(upstreamType)) {
Expand Down
9 changes: 9 additions & 0 deletions packages/next/taskfile.js
Expand Up @@ -241,6 +241,14 @@ export async function ncc_ignore_loader(task, opts) {
.target('compiled/ignore-loader')
}
// eslint-disable-next-line camelcase
externals['is-animated'] = 'next/dist/compiled/is-animated'
export async function ncc_is_animated(task, opts) {
await task
.source(opts.src || relative(__dirname, require.resolve('is-animated')))
.ncc({ packageName: 'is-animated', externals })
.target('compiled/is-animated')
}
// eslint-disable-next-line camelcase
externals['is-docker'] = 'next/dist/compiled/is-docker'
export async function ncc_is_docker(task, opts) {
await task
Expand Down Expand Up @@ -504,6 +512,7 @@ export async function ncc(task) {
'ncc_gzip_size',
'ncc_http_proxy',
'ncc_ignore_loader',
'ncc_is_animated',
'ncc_is_docker',
'ncc_is_wsl',
'ncc_json5',
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
25 changes: 25 additions & 0 deletions test/integration/image-optimizer/test/index.test.js
@@ -1,6 +1,7 @@
/* eslint-env jest */
import fs from 'fs-extra'
import { join } from 'path'
import isAnimated from 'next/dist/compiled/is-animated'
import {
killApp,
findPort,
Expand Down Expand Up @@ -40,6 +41,30 @@ function runTests({ w, isDev }) {
expect(await res.text()).toMatch(/Image Optimizer Home/m)
})

it('should maintain animated gif', async () => {
const query = { w, q: 90, url: '/animated.gif' }
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('image/gif')
expect(isAnimated(await res.buffer())).toBe(true)
})

it('should maintain animated png', async () => {
const query = { w, q: 90, url: '/animated.png' }
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('image/png')
expect(isAnimated(await res.buffer())).toBe(true)
})

it('should maintain animated webp', async () => {
const query = { w, q: 90, url: '/animated.webp' }
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('image/webp')
expect(isAnimated(await res.buffer())).toBe(true)
})

it('should fail when url is missing', async () => {
const query = { w, q: 100 }
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -8878,6 +8878,11 @@ is-alphanumerical@^1.0.0:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"

is-animated@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-animated/-/is-animated-2.0.0.tgz#ffb1f180ff10782b8442dcb0955b5fc915e1a111"
integrity sha512-ZsfhGnSRUjto9owW5WpaDL7z/03H77Ny9JgP/BSyOQ/mAPAKNbdX30WNYbg2R5NhvT5VqKs2KL38znj5OG8tDg==

is-arguments@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
Expand Down