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

feat: adds experimental serverOnlyDependencies property #65415

Closed
wants to merge 6 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: serverOnlyDependencies
description: Mark certain imports as server-only to disable the addition of any subsequent, undesired client-side JS to the client-side bundle.
---

Next.js automatically tree-shakes client dependencies from server components, but there are cases where bundler tree-shaking is not smart enough to solely rely on. For example, tree-shaking works by determining if a dependency is referenced. If a client dependency is referenced (via adding to an object property, logging it, or similar) it will not be tree-shaken and will appear in the resulting client-side bundle.

This is an escape hatch that can be used to mark certain dependencies as server-only, in which case, no subsequent client dependencies will be added to the resulting client-side JS.

```js filename="next.config.js"
const path = require('path')

/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverOnlyDependencies: [path.resolve(__dirname, './app/my-server-dep.js')],
},
}

module.exports = nextConfig
```

In most cases, you should not need to use this property - but it can be helpful to library authors building on Next.js.
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1876,6 +1876,7 @@ export default async function getBaseWebpackConfig(
})
: new FlightClientEntryPlugin({
appDir,
ignore: config.experimental.serverOnlyDependencies ?? [],
dev,
isEdgeServer,
encryptionKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { getAssumedSourceType } from '../loaders/next-flight-loader'
interface Options {
dev: boolean
appDir: string
ignore: string[]
isEdgeServer: boolean
encryptionKey: string
}
Expand Down Expand Up @@ -171,10 +172,12 @@ export class FlightClientEntryPlugin {
encryptionKey: string
isEdgeServer: boolean
assetPrefix: string
ignore: string[]

constructor(options: Options) {
this.dev = options.dev
this.appDir = options.appDir
this.ignore = options.ignore
this.isEdgeServer = options.isEdgeServer
this.assetPrefix = !this.dev && !this.isEdgeServer ? '../' : ''
this.encryptionKey = options.encryptionKey
Expand Down Expand Up @@ -660,7 +663,9 @@ export class FlightClientEntryPlugin {
modRequest = mod.matchResource + ':' + modRequest
}

if (!modRequest) return
// If there is no modRequest, or it's explicitly ignored by `serverOnlyDependencies`,
// we can skip everything from here on for this import
if (!modRequest || this.ignore.includes(modRequest)) return
if (visited.has(modRequest)) {
if (clientComponentImports[modRequest]) {
addClientImport(
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
taint: z.boolean().optional(),
prerenderEarlyExit: z.boolean().optional(),
proxyTimeout: z.number().gte(0).optional(),
serverOnlyDependencies: z.array(z.string()).optional(),
scrollRestoration: z.boolean().optional(),
sri: z
.object({
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ export interface ExperimentalConfig {
}
adjustFontFallbacks?: boolean
adjustFontFallbacksWithSizeAdjust?: boolean
/**
* A list of imports that should completely disable any client dependencies from being added to generated client JS.
*/
serverOnlyDependencies?: string[]

webVitalsAttribution?: Array<(typeof WEB_VITALS)[number]>

Expand Down
39,723 changes: 37,476 additions & 2,247 deletions test/e2e/async-modules/amp-validator-wasm.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use client'

export const UnusedClientComponent = () => {
return <p>better not include me!</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { serverOnlyObject } from './server-only'

export default function Page() {
console.log(serverOnlyObject)
return <p>server only</p>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UnusedClientComponent } from './client'

export const serverOnlyObject = {
client: UnusedClientComponent,
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ describe('app-dir client-components-tree-shaking', () => {
})
if (skipped) return

it('should not add any client dependencies resulting from modules defined in experimental.serverOnlyDependencies', async () => {
const clientChunksDir = join(
next.testDir,
'.next',
'static',
'chunks',
'app',
'server-only-dep'
)
const staticChunksDirents = fs.readdirSync(clientChunksDir, {
withFileTypes: true,
})
const chunkContents = staticChunksDirents
.filter((dirent) => dirent.isFile())
.map((chunkDirent) =>
fs.readFileSync(join(chunkDirent.path, chunkDirent.name), 'utf8')
)

expect(
chunkContents.every((content) => {
return !content.includes('better not include me!')
})
).toBe(true)
})

it('should only include imported components 3rd party package in browser bundle with direct imports', async () => {
const clientChunksDir = join(
next.testDir,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const path = require('path')

module.exports = {
experimental: {
serverOnlyDependencies: [
path.resolve(__dirname, './app/server-only-dep/server-only.js'),
],
},
}
Loading