-
Notifications
You must be signed in to change notification settings - Fork 1
Apply optimization for unused actions (#69178) #4
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
base: pr_034_before
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { type NextInstance } from 'e2e-utils' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function getActionsMappingByRuntime( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next: NextInstance, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runtime: 'node' | 'edge' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const manifest = JSON.parse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await next.readFile('.next/server/server-reference-manifest.json') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return manifest[runtime] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function markLayoutAsEdge(next: NextInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeAll(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await next.stop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const layoutContent = await next.readFile('app/layout.js') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await next.patchFile( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'app/layout.js', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| layoutContent + `\nexport const runtime = 'edge'` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await next.start() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make layout patching idempotent to avoid duplicate exports. If the layout already defines 🔧 Suggested fix export function markLayoutAsEdge(next: NextInstance) {
beforeAll(async () => {
await next.stop()
- const layoutContent = await next.readFile('app/layout.js')
await next.patchFile(
'app/layout.js',
- layoutContent + `\nexport const runtime = 'edge'`
+ (content) => {
+ if (/export const runtime\s*=/.test(content)) {
+ return content.replace(
+ /export const runtime\s*=\s*['"][^'"]+['"]/,
+ `export const runtime = 'edge'`
+ )
+ }
+ return content + `\nexport const runtime = 'edge'`
+ }
)
await next.start()
})
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [route path]: { [layer]: Set<workerId> ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the mapping comment (bracket mismatch + outdated structure). The comment currently mismatches brackets and doesn’t reflect the actual shape used in ✏️ Suggested fix-/*
-{
- [route path]: { [layer]: Set<workerId> ]
-}
-*/
+/*
+{
+ [actionId]: {
+ workers: { [route]: string },
+ layer: { [route]: string }
+ }
+}
+*/📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ActionsMappingOfRuntime = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [actionId: string]: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [route: string]: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| layer: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [route: string]: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ActionState = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [route: string]: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [layer: string]: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getActionsRoutesState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actionsMappingOfRuntime: ActionsMappingOfRuntime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): ActionState { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state: ActionState = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.keys(actionsMappingOfRuntime).forEach((actionId) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const action = actionsMappingOfRuntime[actionId] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const routePaths = Object.keys(action.workers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| routePaths.forEach((routePath) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!state[routePath]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state[routePath] = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const layer = action.layer[routePath] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!state[routePath][layer]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state[routePath][layer] = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state[routePath][layer]++ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate layer presence to avoid If 🔧 Suggested fix routePaths.forEach((routePath) => {
if (!state[routePath]) {
state[routePath] = {}
}
const layer = action.layer[routePath]
+ if (!layer) {
+ throw new Error(
+ `Missing layer for action "${actionId}" on route "${routePath}"`
+ )
+ }
if (!state[routePath][layer]) {
state[routePath][layer] = 0
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function getActionsRoutesStateByRuntime(next: NextInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const actionsMappingOfRuntime = await getActionsMappingByRuntime( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.TEST_EDGE ? 'edge' : 'node' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return getActionsRoutesState(actionsMappingOfRuntime) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use server' | ||
|
|
||
| export async function serverComponentAction() { | ||
| return 'server-action' | ||
| } | ||
|
|
||
| export async function clientComponentAction() { | ||
| return 'client-action' | ||
| } | ||
|
|
||
| export async function unusedExportedAction() { | ||
| return 'unused-exported-action' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
| import { clientComponentAction } from '../actions' | ||
|
|
||
| export default function Page() { | ||
| const [text, setText] = useState('initial') | ||
| return ( | ||
| <div> | ||
| <button | ||
| id="action-1" | ||
| onClick={async () => { | ||
| setText(await clientComponentAction()) | ||
| }} | ||
| > | ||
| Action 1 | ||
| </button> | ||
| <span>{text}</span> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export default function Page() { | ||
| // Inline Server Action | ||
| async function inlineServerAction() { | ||
| 'use server' | ||
| return 'inline-server-action' | ||
| } | ||
|
|
||
| return ( | ||
| <form action={inlineServerAction}> | ||
| <button type="submit">Submit</button> | ||
| </form> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export default function Layout({ children }) { | ||
| return ( | ||
| <html> | ||
| <body>{children}</body> | ||
| </html> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { serverComponentAction } from '../actions' | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <form> | ||
| <input type="text" placeholder="input" /> | ||
| <button formAction={serverComponentAction}>submit</button> | ||
| </form> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,3 @@ | ||||||||||||||||||||||||||||
| process.env.TEST_EDGE = '1' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| require('./basic.test') | ||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restore Setting 🔧 Suggested fix-process.env.TEST_EDGE = '1'
-
-require('./basic.test')
+const prevTestEdge = process.env.TEST_EDGE
+process.env.TEST_EDGE = '1'
+
+require('./basic.test')
+
+if (prevTestEdge === undefined) {
+ delete process.env.TEST_EDGE
+} else {
+ process.env.TEST_EDGE = prevTestEdge
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { nextTestSetup } from 'e2e-utils' | ||
| import { | ||
| getActionsRoutesStateByRuntime, | ||
| markLayoutAsEdge, | ||
| } from '../_testing/utils' | ||
|
|
||
| describe('actions-tree-shaking - basic', () => { | ||
| const { next } = nextTestSetup({ | ||
| files: __dirname, | ||
| }) | ||
|
|
||
| if (process.env.TEST_EDGE) { | ||
| markLayoutAsEdge(next) | ||
| } | ||
|
|
||
| it('should not have the unused action in the manifest', async () => { | ||
| const actionsRoutesState = await getActionsRoutesStateByRuntime(next) | ||
|
|
||
| expect(actionsRoutesState).toMatchObject({ | ||
| // only one server layer action | ||
| 'app/server/page': { | ||
| rsc: 1, | ||
| }, | ||
| // only one browser layer action | ||
| 'app/client/page': { | ||
| 'action-browser': 1, | ||
| }, | ||
| 'app/inline/page': { | ||
| rsc: 1, | ||
| }, | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export default function Layout({ children }) { | ||
| return ( | ||
| <html> | ||
| <body>{children}</body> | ||
| </html> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use server' | ||
|
|
||
| export async function esmModuleTypeAction() { | ||
| return 'esm-module-type-action' | ||
| } | ||
|
|
||
| export async function cjsModuleTypeAction() { | ||
| return 'cjs-module-type-action' | ||
| } | ||
|
|
||
| export async function unusedModuleTypeAction1() { | ||
| return 'unused-module-type-action-1' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| const { cjsModuleTypeAction } = require('./actions') | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <div> | ||
| <h3>One</h3> | ||
| <form> | ||
| <input type="text" placeholder="input" /> | ||
| <button formAction={cjsModuleTypeAction}>submit</button> | ||
| </form> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use server' | ||
|
|
||
| export async function esmModuleTypeAction() { | ||
| return 'esm-module-type-action' | ||
| } | ||
|
|
||
| export async function cjsModuleTypeAction() { | ||
| return 'cjs-module-type-action' | ||
| } | ||
|
|
||
| export async function unusedModuleTypeAction1() { | ||
| return 'unused-module-type-action-1' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { esmModuleTypeAction } from './actions' | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <div> | ||
| <h3>One</h3> | ||
| <form> | ||
| <input type="text" placeholder="input" /> | ||
| <button formAction={esmModuleTypeAction}>submit</button> | ||
| </form> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| process.env.TEST_EDGE = '1' | ||
|
|
||
| require('./mixed-module-actions.test') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { nextTestSetup } from 'e2e-utils' | ||
| import { | ||
| getActionsRoutesStateByRuntime, | ||
| markLayoutAsEdge, | ||
| } from '../_testing/utils' | ||
|
|
||
| describe('actions-tree-shaking - mixed-module-actions', () => { | ||
| const { next } = nextTestSetup({ | ||
| files: __dirname, | ||
| }) | ||
|
|
||
| if (process.env.TEST_EDGE) { | ||
| markLayoutAsEdge(next) | ||
| } | ||
|
|
||
| it('should not do tree shake for cjs module when import server actions', async () => { | ||
| const actionsRoutesState = await getActionsRoutesStateByRuntime(next) | ||
|
|
||
| expect(actionsRoutesState).toMatchObject({ | ||
| 'app/mixed-module/esm/page': { | ||
| rsc: 1, | ||
| }, | ||
| // CJS import is not able to tree shake, so it will include all actions | ||
| 'app/mixed-module/cjs/page': { | ||
| rsc: 3, | ||
| }, | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export default function Layout({ children }) { | ||
| return ( | ||
| <html> | ||
| <body>{children}</body> | ||
| </html> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use server' | ||
|
|
||
| export async function sharedClientLayerAction() { | ||
| return 'shared-client-layer-action' | ||
| } | ||
|
|
||
| export async function unusedClientLayerAction1() { | ||
| return 'unused-client-layer-action-1' | ||
| } | ||
|
|
||
| export async function unusedClientLayerAction2() { | ||
| return 'unused-client-layer-action-2' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
| import { sharedClientLayerAction } from './reexport-action' | ||
|
|
||
| export default function Page() { | ||
| const [text, setText] = useState('initial') | ||
| return ( | ||
| <div> | ||
| <button | ||
| id="action-1" | ||
| onClick={async () => { | ||
| setText(await sharedClientLayerAction()) | ||
| }} | ||
| > | ||
| Action 1 | ||
| </button> | ||
| <span>{text}</span> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { | ||
| sharedClientLayerAction, | ||
| unusedClientLayerAction1, | ||
| unusedClientLayerAction2, | ||
| } from './actions' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use server' | ||
|
|
||
| export async function sharedServerLayerAction() { | ||
| return 'shared-server-layer-action' | ||
| } | ||
|
|
||
| export async function unusedServerLayerAction1() { | ||
| return 'unused-server-layer-action-1' | ||
| } | ||
|
|
||
| export async function unusedServerLayerAction2() { | ||
| return 'unused-server-layer-action-2' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { sharedServerLayerAction } from './reexport-action' | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <div> | ||
| <form> | ||
| <input type="text" placeholder="input" /> | ||
| <button formAction={sharedServerLayerAction}>submit</button> | ||
| </form> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export { | ||
| sharedServerLayerAction, | ||
| unusedServerLayerAction1, | ||
| unusedServerLayerAction2, | ||
| } from './actions' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use server' | ||
|
|
||
| export async function sharedClientLayerAction() { | ||
| return 'shared-client-layer-action' | ||
| } | ||
|
|
||
| export async function unusedClientLayerAction1() { | ||
| return 'unused-client-layer-action-1' | ||
| } | ||
|
|
||
| export async function unusedClientLayerAction2() { | ||
| return 'unused-client-layer-action-2' | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a guard for missing runtime mapping.
If the manifest doesn’t contain the requested runtime key, the later code will throw a cryptic error. Failing fast with a clear message makes test failures much easier to diagnose.
🔧 Suggested fix
async function getActionsMappingByRuntime( next: NextInstance, runtime: 'node' | 'edge' ) { const manifest = JSON.parse( await next.readFile('.next/server/server-reference-manifest.json') ) - - return manifest[runtime] + const mapping = manifest?.[runtime] + if (!mapping) { + throw new Error(`Missing server-reference-manifest runtime: ${runtime}`) + } + return mapping }📝 Committable suggestion
🤖 Prompt for AI Agents