Skip to content

Commit

Permalink
Simplify Server Action Webpack plugin (#71721)
Browse files Browse the repository at this point in the history
This PR simplifies our `flight-client-entry-plugin` to not re-compute
the server reference IDs because they're already provided in `buildInfo`
via our SWC transform. It's also a small performance win depending on
the number of Server Actions in the project.

Cherry-picked from #71463.
  • Loading branch information
shuding authored Oct 24, 2024
1 parent ddc8807 commit 5ab28cd
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 58 deletions.
1 change: 1 addition & 0 deletions packages/next/src/build/analysis/get-page-static-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export function getRSCModuleInformation(
return {
type,
actions,
actionIds: parsedActionsMeta,
clientRefs,
clientEntryType,
isClientRef,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { generateActionId } from './utils'

export type NextFlightActionEntryLoaderOptions = {
actions: string
encryptionKey: string
}

function nextFlightActionEntryLoader(this: any) {
const { actions, encryptionKey }: NextFlightActionEntryLoaderOptions =
this.getOptions()
const { actions }: NextFlightActionEntryLoaderOptions = this.getOptions()

const actionList = JSON.parse(actions) as [string, string[]][]
const actionList = JSON.parse(actions) as [
string,
[id: string, name: string][],
][]
const individualActions = actionList
.map(([path, names]) => {
return names.map((name) => {
const id = generateActionId(encryptionKey, path, name)
return [id, path, name] as [string, string, string]
.map(([path, actionsFromModule]) => {
return actionsFromModule.map(([id, name]) => {
return [id, path, name]
})
})
.flat()
Expand Down
19 changes: 4 additions & 15 deletions packages/next/src/build/webpack/loaders/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type webpack from 'webpack'
import { createHash } from 'crypto'
import { RSC_MODULE_TYPES } from '../../../shared/lib/constants'

const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif', 'ico', 'svg']
Expand Down Expand Up @@ -47,25 +46,15 @@ export function isCSSMod(mod: {
export function getActionsFromBuildInfo(mod: {
resource: string
buildInfo?: any
}): undefined | string[] {
return mod.buildInfo?.rsc?.actions
}): undefined | Record<string, string> {
return mod.buildInfo?.rsc?.actionIds
}

export function generateActionId(
hashSalt: string,
filePath: string,
exportName: string
) {
return createHash('sha1')
.update(hashSalt + filePath + ':' + exportName)
.digest('hex')
}

export function encodeToBase64<T extends {}>(obj: T): string {
export function encodeToBase64<T extends object>(obj: T): string {
return Buffer.from(JSON.stringify(obj)).toString('base64')
}

export function decodeFromBase64<T extends {}>(str: string): T {
export function decodeFromBase64<T extends object>(str: string): T {
return JSON.parse(Buffer.from(str, 'base64').toString('utf8'))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
} from '../../../shared/lib/constants'
import {
getActionsFromBuildInfo,
generateActionId,
isClientComponentEntryModule,
isCSSMod,
regexCSS,
Expand All @@ -43,6 +42,8 @@ import { getModuleBuildInfo } from '../loaders/get-module-build-info'
import { getAssumedSourceType } from '../loaders/next-flight-loader'
import { isAppRouteRoute } from '../../../lib/is-app-route-route'

type ActionIdNamePair = [id: string, name: string]

interface Options {
dev: boolean
appDir: string
Expand Down Expand Up @@ -283,14 +284,17 @@ export class FlightClientEntryPlugin {

const addActionEntryList: Array<ReturnType<typeof this.injectActionEntry>> =
[]
const actionMapsPerEntry: Record<string, Map<string, string[]>> = {}
const createdActions = new Set<string>()
const actionMapsPerEntry: Record<
string,
Map<string, ActionIdNamePair[]>
> = {}
const createdActionIds = new Set<string>()

// For each SC server compilation entry, we need to create its corresponding
// client component entry.
forEachEntryModule(compilation, ({ name, entryModule }) => {
const internalClientComponentEntryImports: ClientComponentImports = {}
const actionEntryImports = new Map<string, string[]>()
const actionEntryImports = new Map<string, ActionIdNamePair[]>()
const clientEntriesToInject = []
const mergedCSSimports: CssImports = {}

Expand All @@ -310,8 +314,8 @@ export class FlightClientEntryPlugin {
resolvedModule: connection.resolvedModule,
})

actionImports.forEach(([dep, names]) =>
actionEntryImports.set(dep, names)
actionImports.forEach(([dep, actions]) =>
actionEntryImports.set(dep, actions)
)

const isAbsoluteRequest = path.isAbsolute(entryRequest)
Expand Down Expand Up @@ -429,7 +433,7 @@ export class FlightClientEntryPlugin {
actions: actionEntryImports,
entryName: name,
bundlePath: name,
createdActions,
createdActionIds,
})
)
}
Expand Down Expand Up @@ -458,7 +462,10 @@ export class FlightClientEntryPlugin {
await Promise.all(addActionEntryList)

const addedClientActionEntryList: Promise<any>[] = []
const actionMapsPerClientEntry: Record<string, Map<string, string[]>> = {}
const actionMapsPerClientEntry: Record<
string,
Map<string, ActionIdNamePair[]>
> = {}

// We need to create extra action entries that are created from the
// client layer.
Expand All @@ -484,21 +491,21 @@ export class FlightClientEntryPlugin {
}
}

for (const [name, actionEntryImports] of Object.entries(
for (const [entryName, actionEntryImports] of Object.entries(
actionMapsPerClientEntry
)) {
// If an action method is already created in the server layer, we don't
// need to create it again in the action layer.
// This is to avoid duplicate action instances and make sure the module
// state is shared.
let remainingClientImportedActions = false
const remainingActionEntryImports = new Map<string, string[]>()
for (const [dep, actionNames] of actionEntryImports) {
const remainingActionEntryImports = new Map<string, ActionIdNamePair[]>()
for (const [dep, actions] of actionEntryImports) {
const remainingActionNames = []
for (const actionName of actionNames) {
const id = name + '@' + dep + '@' + actionName
if (!createdActions.has(id)) {
remainingActionNames.push(actionName)
for (const action of actions) {
// `action` is a [id, name] pair.
if (!createdActionIds.has(entryName + '@' + action[0])) {
remainingActionNames.push(action)
}
}
if (remainingActionNames.length > 0) {
Expand All @@ -513,10 +520,10 @@ export class FlightClientEntryPlugin {
compiler,
compilation,
actions: remainingActionEntryImports,
entryName: name,
bundlePath: name,
entryName,
bundlePath: entryName,
fromClient: true,
createdActions,
createdActionIds,
})
)
}
Expand All @@ -533,7 +540,7 @@ export class FlightClientEntryPlugin {
dependencies: ReturnType<typeof webpack.EntryPlugin.createDependency>[]
}) {
// action file path -> action names
const collectedActions = new Map<string, string[]>()
const collectedActions = new Map<string, ActionIdNamePair[]>()

// Keep track of checked modules to avoid infinite loops with recursive imports.
const visitedModule = new Set<string>()
Expand All @@ -558,7 +565,7 @@ export class FlightClientEntryPlugin {

const actions = getActionsFromBuildInfo(mod)
if (actions) {
collectedActions.set(modResource, actions)
collectedActions.set(modResource, Object.entries(actions))
}

// Collect used exported actions transversely.
Expand Down Expand Up @@ -617,14 +624,14 @@ export class FlightClientEntryPlugin {
}): {
cssImports: CssImports
clientComponentImports: ClientComponentImports
actionImports: [string, string[]][]
actionImports: [string, ActionIdNamePair[]][]
} {
// Keep track of checked modules to avoid infinite loops with recursive imports.
const visitedOfClientComponentsTraverse = new Set()

// Info to collect.
const clientComponentImports: ClientComponentImports = {}
const actionImports: [string, string[]][] = []
const actionImports: [string, ActionIdNamePair[]][] = []
const CSSImports = new Set<string>()

const filterClientComponents = (
Expand Down Expand Up @@ -652,7 +659,7 @@ export class FlightClientEntryPlugin {

const actions = getActionsFromBuildInfo(mod)
if (actions) {
actionImports.push([modResource, actions])
actionImports.push([modResource, Object.entries(actions)])
}

if (isCSSMod(mod)) {
Expand Down Expand Up @@ -839,20 +846,20 @@ export class FlightClientEntryPlugin {
entryName,
bundlePath,
fromClient,
createdActions,
createdActionIds,
}: {
compiler: webpack.Compiler
compilation: webpack.Compilation
actions: Map<string, string[]>
actions: Map<string, ActionIdNamePair[]>
entryName: string
bundlePath: string
createdActions: Set<string>
createdActionIds: Set<string>
fromClient?: boolean
}) {
const actionsArray = Array.from(actions.entries())
for (const [dep, actionNames] of actions) {
for (const actionName of actionNames) {
createdActions.add(entryName + '@' + dep + '@' + actionName)
for (const [, actionsFromModule] of actions) {
for (const [id] of actionsFromModule) {
createdActionIds.add(entryName + '@' + id)
}
}

Expand All @@ -862,17 +869,15 @@ export class FlightClientEntryPlugin {

const actionLoader = `next-flight-action-entry-loader?${stringify({
actions: JSON.stringify(actionsArray),
encryptionKey: this.encryptionKey,
__client_imported__: fromClient,
})}!`

const currentCompilerServerActions = this.isEdgeServer
? pluginState.edgeServerActions
: pluginState.serverActions

for (const [actionFilePath, actionNames] of actionsArray) {
for (const name of actionNames) {
const id = generateActionId(this.encryptionKey, actionFilePath, name)
for (const [, actionsFromModule] of actionsArray) {
for (const [id] of actionsFromModule) {
if (typeof currentCompilerServerActions[id] === 'undefined') {
currentCompilerServerActions[id] = {
workers: {},
Expand Down

0 comments on commit 5ab28cd

Please sign in to comment.