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

Fix tree-shaking for metadata image functions on the Edge runtime #51762

Merged
merged 6 commits into from
Jun 26, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2272,6 +2272,12 @@ export default async function getBaseWebpackConfig(
"'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component.",
},
},
{
// Mark `image-response.js` as side-effects free to make sure we can
// tree-shake it if not used.
test: /[\\/]next[\\/]dist[\\/](esm[\\/])?server[\\/]web[\\/]exports[\\/]image-response\.js/,
sideEffects: false,
},
].filter(Boolean),
},
plugins: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* This loader is responsible for extracting the metadata image info for rendering in html
*/

import type webpack from 'webpack'
import type {
MetadataImageModule,
PossibleImageFileNameConvention,
Expand All @@ -12,6 +13,7 @@ import loaderUtils from 'next/dist/compiled/loader-utils3'
import { getImageSize } from '../../../server/image-optimizer'
import { imageExtMimeTypeMap } from '../../../lib/mime-type'
import { fileExists } from '../../../lib/file-exists'
import { WEBPACK_RESOURCE_QUERIES } from '../../../lib/constants'

interface Options {
segment: string
Expand Down Expand Up @@ -52,12 +54,55 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {
const pathnamePrefix = path.join(basePath, segment)

if (isDynamicResource) {
const mod = await new Promise<webpack.NormalModule>((res, rej) => {
this.loadModule(
resourcePath,
(err: null | Error, _source: any, _sourceMap: any, module: any) => {
if (err) {
return rej(err)
}
res(module)
}
)
})

const exportedFieldsExcludingDefault =
mod.dependencies
?.filter((dep) => {
return (
[
'HarmonyExportImportedSpecifierDependency',
'HarmonyExportSpecifierDependency',
].includes(dep.constructor.name) &&
'name' in dep &&
dep.name !== 'default'
)
})
.map((dep: any) => {
return dep.name
}) || []
// re-export and spread as `exportedImageData` to avoid non-exported error
return `\
import * as exported from ${JSON.stringify(resourcePath)}
import {
${exportedFieldsExcludingDefault
.map((field) => `${field} as _${field}`)
.join(',')}
} from ${JSON.stringify(
// This is an arbitrary resource query to ensure it's a new request, instead
// of sharing the same module with next-metadata-route-loader.
// Since here we only need export fields such as `size`, `alt` and
// `generateImageMetadata`, avoid sharing the same module can make this entry
// smaller.
resourcePath + '?' + WEBPACK_RESOURCE_QUERIES.metadataImageMeta
shuding marked this conversation as resolved.
Show resolved Hide resolved
)}
import { fillMetadataSegment } from 'next/dist/lib/metadata/get-metadata-route'

const imageModule = { ...exported }
const imageModule = {
${exportedFieldsExcludingDefault
.map((field) => `${field}: _${field}`)
.join(',')}
}

export default async function (props) {
const { __metadata_id__: _, ...params } = props.params
const imageUrl = fillMetadataSegment(${JSON.stringify(
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,5 @@ export const WEBPACK_LAYERS = {
export const WEBPACK_RESOURCE_QUERIES = {
edgeSSREntry: '__next_edge_ssr_entry__',
metadata: '__next_metadata__',
metadataImageMeta: '__next_metadata_image_meta__',
}
22 changes: 22 additions & 0 deletions test/e2e/app-dir/metadata-edge/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default function Layout({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}

export const metadata = {
metadataBase: new URL('https://mydomain.com'),
title: 'Next.js App',
description: 'This is a Next.js App',
twitter: {
cardType: 'summary_large_image',
title: 'Twitter - Next.js App',
description: 'Twitter - This is a Next.js App',
},
alternates: {
canonical: './',
},
}
23 changes: 23 additions & 0 deletions test/e2e/app-dir/metadata-edge/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ImageResponse } from 'next/server'

export const alt = 'Open Graph'

export default function og() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 128,
background: 'lavender',
}}
>
Open Graph
</div>
)
)
}
11 changes: 11 additions & 0 deletions test/e2e/app-dir/metadata-edge/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'

export default function Page() {
return <>hello index</>
}

export const metadata = {
title: 'index page',
}

export const runtime = 'edge'
30 changes: 30 additions & 0 deletions test/e2e/app-dir/metadata-edge/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createNextDescribe } from 'e2e-utils'
import imageSize from 'image-size'

createNextDescribe(
'app dir - Metadata API on the Edge runtime',
{
files: __dirname,
},
({ next, isNextStart }) => {
describe('OG image route', () => {
if (isNextStart) {
it('should not bundle `ImageResponse` into the page worker', async () => {
const pageBundle = await next.readFile(
'.next/server/middleware-manifest.json'
)
expect(pageBundle).not.toContain('ImageResponse')
})
}
})

it('should render OpenGraph image meta tag correctly', async () => {
const html$ = await next.render$('/')
const ogUrl = new URL(html$('meta[property="og:image"]').attr('content'))
const imageBuffer = await (await next.fetch(ogUrl.pathname)).buffer()

const size = imageSize(imageBuffer)
expect([size.width, size.height]).toEqual([1200, 630])
})
}
)
9 changes: 9 additions & 0 deletions test/e2e/app-dir/metadata-edge/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {}

// For development: analyze the bundled chunks for stats app
if (process.env.ANALYZE) {
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: true,
})
module.exports = withBundleAnalyzer(module.exports)
}