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

Change the Server Actions feature flag to be validated at compile time #52147

Merged
merged 2 commits into from
Jul 3, 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
25 changes: 25 additions & 0 deletions packages/next-swc/crates/core/src/server_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use turbopack_binding::swc::core::{
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Config {
pub is_server: bool,
pub enabled: bool
}

pub fn server_actions<C: Comments>(
Expand Down Expand Up @@ -101,6 +102,7 @@ impl<C: Comments> ServerActions<C> {
&mut body.stmts,
remove_directive,
&mut is_action_fn,
self.config.enabled,
);

if is_action_fn && !self.config.is_server {
Expand Down Expand Up @@ -721,6 +723,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
stmts,
&mut self.in_action_file,
&mut self.has_action,
self.config.enabled,
);

let old_annotations = self.annotations.take();
Expand Down Expand Up @@ -1231,6 +1234,7 @@ fn remove_server_directive_index_in_module(
stmts: &mut Vec<ModuleItem>,
in_action_file: &mut bool,
has_action: &mut bool,
enabled: bool,
) {
let mut is_directive = true;

Expand All @@ -1244,6 +1248,16 @@ fn remove_server_directive_index_in_module(
if is_directive {
*in_action_file = true;
*has_action = true;
if !enabled {
HANDLER.with(|handler| {
handler
.struct_span_err(
*span,
"To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention",
)
.emit()
});
}
return false;
} else {
HANDLER.with(|handler| {
Expand Down Expand Up @@ -1320,6 +1334,7 @@ fn remove_server_directive_index_in_fn(
stmts: &mut Vec<Stmt>,
remove_directive: bool,
is_action_fn: &mut bool,
enabled: bool,
) {
let mut is_directive = true;

Expand All @@ -1332,6 +1347,16 @@ fn remove_server_directive_index_in_fn(
if value == "use server" {
if is_directive {
*is_action_fn = true;
if !enabled {
HANDLER.with(|handler| {
handler
.struct_span_err(
*span,
"To use Server Actions, please enable the feature flag in your Next.js config. Read more: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions#convention",
)
.emit()
});
}
if remove_directive {
return false;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/build/swc/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function getBaseSWCOptions({
swcCacheDir,
isServerLayer,
hasServerComponents,
isServerActionsEnabled,
}: {
filename: string
jest?: boolean
Expand All @@ -57,6 +58,7 @@ function getBaseSWCOptions({
swcCacheDir?: string
isServerLayer?: boolean
hasServerComponents?: boolean
isServerActionsEnabled?: boolean
}) {
const parserConfig = getParserOptions({ filename, jsConfig })
const paths = jsConfig?.compilerOptions?.paths
Expand Down Expand Up @@ -157,6 +159,8 @@ function getBaseSWCOptions({
: undefined,
serverActions: hasServerComponents
? {
// TODO-APP: When Server Actions is stable, we need to remove this flag.
enabled: !!isServerActionsEnabled,
isServer: !!isServerLayer,
}
: undefined,
Expand Down Expand Up @@ -285,6 +289,7 @@ export function getLoaderSWCOptions({
relativeFilePathFromRoot,
hasServerComponents,
isServerLayer,
isServerActionsEnabled,
}: // This is not passed yet as "paths" resolving is handled by webpack currently.
// resolvedBaseUrl,
{
Expand All @@ -304,6 +309,7 @@ export function getLoaderSWCOptions({
relativeFilePathFromRoot: string
hasServerComponents?: boolean
isServerLayer: boolean
isServerActionsEnabled?: boolean
}) {
let baseOptions: any = getBaseSWCOptions({
filename,
Expand All @@ -318,6 +324,7 @@ export function getLoaderSWCOptions({
swcCacheDir,
hasServerComponents,
isServerLayer,
isServerActionsEnabled,
})
baseOptions.fontLoaders = {
fontLoaders: [
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack/loaders/next-swc-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ async function loaderTransform(
swcCacheDir,
relativeFilePathFromRoot,
hasServerComponents,
isServerActionsEnabled: nextConfig?.experimental?.serverActions,
isServerLayer,
})

Expand Down
135 changes: 62 additions & 73 deletions packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,21 +350,13 @@ export class FlightClientEntryPlugin {
)

if (actionEntryImports.size > 0) {
if (!this.useServerActions) {
compilation.errors.push(
new Error(
'Server Actions require `experimental.serverActions` option to be enabled in your Next.js config: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions'
)
)
} else {
if (!actionMapsPerEntry[name]) {
actionMapsPerEntry[name] = new Map()
}
actionMapsPerEntry[name] = new Map([
...actionMapsPerEntry[name],
...actionEntryImports,
])
if (!actionMapsPerEntry[name]) {
actionMapsPerEntry[name] = new Map()
}
actionMapsPerEntry[name] = new Map([
...actionMapsPerEntry[name],
...actionEntryImports,
])
}
})

Expand All @@ -388,31 +380,28 @@ export class FlightClientEntryPlugin {
)
}

compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, () => {
const addedClientActionEntryList: Promise<any>[] = []
const actionMapsPerClientEntry: Record<string, Map<string, string[]>> = {}

// We need to create extra action entries that are created from the
// client layer.
// Start from each entry's created SSR dependency from our previous step.
for (const [name, ssrEntryDepdendencies] of Object.entries(
createdSSRDependenciesForEntry
)) {
// Collect from all entries, e.g. layout.js, page.js, loading.js, ...
// add agregate them.
const actionEntryImports = this.collectClientActionsFromDependencies({
compilation,
dependencies: ssrEntryDepdendencies,
})
if (this.useServerActions) {
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, () => {
const addedClientActionEntryList: Promise<any>[] = []
const actionMapsPerClientEntry: Record<
string,
Map<string, string[]>
> = {}

// We need to create extra action entries that are created from the
// client layer.
// Start from each entry's created SSR dependency from our previous step.
for (const [name, ssrEntryDepdendencies] of Object.entries(
createdSSRDependenciesForEntry
)) {
// Collect from all entries, e.g. layout.js, page.js, loading.js, ...
// add agregate them.
const actionEntryImports = this.collectClientActionsFromDependencies({
compilation,
dependencies: ssrEntryDepdendencies,
})

if (actionEntryImports.size > 0) {
if (!this.useServerActions) {
compilation.errors.push(
new Error(
'Server Actions require `experimental.serverActions` option to be enabled in your Next.js config: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions'
)
)
} else {
if (actionEntryImports.size > 0) {
if (!actionMapsPerClientEntry[name]) {
actionMapsPerClientEntry[name] = new Map()
}
Expand All @@ -422,47 +411,47 @@ export class FlightClientEntryPlugin {
])
}
}
}

for (const [name, 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 remainingActionNames = []
for (const actionName of actionNames) {
const id = name + '@' + dep + '@' + actionName
if (!createdActions.has(id)) {
remainingActionNames.push(actionName)
for (const [name, 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 remainingActionNames = []
for (const actionName of actionNames) {
const id = name + '@' + dep + '@' + actionName
if (!createdActions.has(id)) {
remainingActionNames.push(actionName)
}
}
if (remainingActionNames.length > 0) {
remainingActionEntryImports.set(dep, remainingActionNames)
remainingClientImportedActions = true
}
}
if (remainingActionNames.length > 0) {
remainingActionEntryImports.set(dep, remainingActionNames)
remainingClientImportedActions = true
}
}

if (remainingClientImportedActions) {
addedClientActionEntryList.push(
this.injectActionEntry({
compiler,
compilation,
actions: remainingActionEntryImports,
entryName: name,
bundlePath: name,
fromClient: true,
})
)
if (remainingClientImportedActions) {
addedClientActionEntryList.push(
this.injectActionEntry({
compiler,
compilation,
actions: remainingActionEntryImports,
entryName: name,
bundlePath: name,
fromClient: true,
})
)
}
}
}

return Promise.all(addedClientActionEntryList)
})
return Promise.all(addedClientActionEntryList)
})
}

// Invalidate in development to trigger recompilation
const invalidator = getInvalidator(compiler.outputPath)
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/app-dir/actions/app-action-invalid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ createNextDescribe(

it('should error if serverActions is not enabled', async () => {
expect(next.cliOutput).toContain(
'Server Actions require `experimental.serverActions` option'
'To use Server Actions, please enable the feature flag in your Next.js config.'
)
})
}
Expand Down