Skip to content

Commit

Permalink
Fix tree shaking for image generation module (#51950)
Browse files Browse the repository at this point in the history
### Issue

When the og module is a shared module being imported in both page and metadata image routes, it will be shared in the module graph. Especially in the edge runtime, since the `default` export is being used in the metadata image routes, then it can't be easily tree-shaked out.

### Solution

Separate the image route to a separate layer which won't share modules with the page, so that image route is always bundling separately and the `@vercel/og` module only stays inside in that layer, when import image metadata named exports (size / alt / etc..) it can be still tree shaked.

Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
  • Loading branch information
shuding and huozhi committed Jul 8, 2023
1 parent ca132f6 commit 32d7795
Show file tree
Hide file tree
Showing 12 changed files with 76 additions and 43 deletions.
16 changes: 15 additions & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,16 @@ export default async function getBaseWebpackConfig(
} as any,
]
: []),
...(hasAppDir
? [
{
resourceQuery: new RegExp(
WEBPACK_RESOURCE_QUERIES.metadataRoute
),
layer: WEBPACK_LAYERS.metadataImage,
},
]
: []),
...(hasAppDir && isEdgeServer
? [
// The Edge bundle includes the server in its entrypoint, so it has to
Expand Down Expand Up @@ -2181,7 +2191,11 @@ export default async function getBaseWebpackConfig(
issuer: { not: regexLikeCss },
dependency: { not: ['url'] },
resourceQuery: {
not: [new RegExp(WEBPACK_RESOURCE_QUERIES.metadata)],
not: [
new RegExp(WEBPACK_RESOURCE_QUERIES.metadata),
new RegExp(WEBPACK_RESOURCE_QUERIES.metadataRoute),
new RegExp(WEBPACK_RESOURCE_QUERIES.metadataImageMeta),
],
},
options: {
isDev: dev,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export async function createStaticMetadataFromRoute(
basePath,
pageExtensions,
}
// WEBPACK_RESOURCE_QUERIES.metadata query here only for filtering out applying to image loader
)}!${filepath}?${WEBPACK_RESOURCE_QUERIES.metadata}`

const imageModule = `(async (props) => (await import(/* webpackMode: "eager" */ ${JSON.stringify(
Expand Down
8 changes: 6 additions & 2 deletions packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { RouteKind } from '../../../server/future/route-kind'
import { AppRouteRouteModuleOptions } from '../../../server/future/route-modules/app-route/module'
import { AppBundlePathNormalizer } from '../../../server/future/normalizers/built/app/app-bundle-path-normalizer'
import { MiddlewareConfig } from '../../analysis/get-page-static-info'
import { getFilenameAndExtension } from './next-metadata-route-loader'

export type AppLoaderOptions = {
name: string
Expand Down Expand Up @@ -105,10 +106,13 @@ async function createAppRouteCode({
// the route to ensure that the route is generated.
const filename = path.parse(resolvedPagePath).name
if (isMetadataRoute(name) && filename !== 'route') {
const { ext } = getFilenameAndExtension(resolvedPagePath)
const isDynamic = pageExtensions.includes(ext)

resolvedPagePath = `next-metadata-route-loader?${stringify({
page,
pageExtensions,
})}!${resolvedPagePath + '?' + WEBPACK_RESOURCE_QUERIES.metadata}`
isDynamic: isDynamic ? '1' : '0',
})}!${resolvedPagePath}${`?${WEBPACK_RESOURCE_QUERIES.metadataRoute}`}`
}

// References the route handler file to load found in `./routes/${kind}.ts`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ const EdgeAppRouteLoader: webpack.LoaderDefinitionFunction<EdgeAppRouteLoaderQue
const modulePath = `${appDirLoader}${stringifiedPagePath.substring(
1,
stringifiedPagePath.length - 1
)}?${WEBPACK_RESOURCE_QUERIES.edgeSSREntry}&${
WEBPACK_RESOURCE_QUERIES.metadata
}`
)}?${WEBPACK_RESOURCE_QUERIES.edgeSSREntry}`

return `
import { EdgeRouteModuleWrapper } from 'next/dist/esm/server/web/edge-route-module-wrapper'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ async function nextMetadataImageLoader(this: any, content: Buffer) {
.map((dep: any) => {
return dep.name
}) || []

// re-export and spread as `exportedImageData` to avoid non-exported error
return `\
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type webpack from 'webpack'
import fs from 'fs'
import path from 'path'
import { imageExtMimeTypeMap } from '../../../lib/mime-type'
import { WEBPACK_RESOURCE_QUERIES } from '../../../lib/constants'

const cacheHeader = {
none: 'no-cache, no-store',
Expand All @@ -12,10 +11,10 @@ const cacheHeader = {

type MetadataRouteLoaderOptions = {
page: string
pageExtensions: string[]
isDynamic: '1' | '0'
}

function getFilenameAndExtension(resourcePath: string) {
export function getFilenameAndExtension(resourcePath: string) {
const filename = path.basename(resourcePath)
const [name, ext] = filename.split('.')
return { name, ext }
Expand Down Expand Up @@ -49,11 +48,7 @@ import { NextResponse } from 'next/server'
const contentType = ${JSON.stringify(getContentType(resourcePath))}
const buffer = Buffer.from(${JSON.stringify(
(
await fs.promises.readFile(
resourcePath.replace('?' + WEBPACK_RESOURCE_QUERIES.metadata, '')
)
).toString('base64')
(await fs.promises.readFile(resourcePath)).toString('base64')
)}, 'base64'
)
Expand Down Expand Up @@ -205,13 +200,11 @@ ${staticGenerationCode}
const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction<MetadataRouteLoaderOptions> =
async function () {
const { resourcePath } = this
const { pageExtensions, page } = this.getOptions()

const { name: fileBaseName, ext } = getFilenameAndExtension(resourcePath)
const isDynamic = pageExtensions.includes(ext)
const { page, isDynamic } = this.getOptions()
const { name: fileBaseName } = getFilenameAndExtension(resourcePath)

let code = ''
if (isDynamic) {
if (isDynamic === '1') {
if (fileBaseName === 'robots' || fileBaseName === 'manifest') {
code = getDynamicTextRouteCode(resourcePath)
} else if (fileBaseName === 'sitemap') {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ export const WEBPACK_LAYERS = {
middleware: 'middleware',
edgeAsset: 'edge-asset',
appClient: 'app-client',
metadataImage: 'app-metadata-image',
}

export const WEBPACK_RESOURCE_QUERIES = {
edgeSSREntry: '__next_edge_ssr_entry__',
metadata: '__next_metadata__',
metadataRoute: '__next_metadata_route__',
metadataImageMeta: '__next_metadata_image_meta__',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const alt = 'Open Graph Edge'

export const runtime = 'edge'

export { default } from '../og'
11 changes: 11 additions & 0 deletions test/e2e/app-dir/metadata-edge/app/another/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 another</>
}

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

export const runtime = 'edge'
21 changes: 21 additions & 0 deletions test/e2e/app-dir/metadata-edge/app/og.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ImageResponse } from 'next/server'

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>
)
)
}
22 changes: 1 addition & 21 deletions test/e2e/app-dir/metadata-edge/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,3 @@
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>
)
)
}
export { default } from './og'
9 changes: 6 additions & 3 deletions test/e2e/app-dir/metadata-edge/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ createNextDescribe(
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'
)
const pageBundle = await next.readFile('.next/server/app/page.js')
expect(pageBundle).not.toContain('ImageResponse')

const sharedPageBundle = await next.readFile(
'.next/server/app/another/page.js'
)
expect(sharedPageBundle).not.toContain('ImageResponse')
})
}
})
Expand Down

0 comments on commit 32d7795

Please sign in to comment.