Repository: vercel/next.js
Pull request: Emit build error when `'use cache'` directive is misspelled

Handles cases like:
- `'use cache:remote'` (missing space after colon)
- `'use cache : default'` (extra space before colon)
- `'use cache private'` (missing colon)
- `'use cache: '` (missing cache kind)

closes NAR-392

Changed files:
filename: crates/next-custom-transforms/src/transforms/server_actions.rs
patch: @@ -2739,7 +2739,7 @@ impl DirectiveVisitor<'_> {
                     });
                 } else
                 // `use cache` or `use cache: foo`
-                if value == "use cache" || value.starts_with("use cache: ") {
+                if let Some(rest) = value.strip_prefix("use cache") {
                     // Increment telemetry counter tracking usage of "use cache" directives
 
                     if in_fn_body && !allow_inline {
@@ -2759,25 +2759,59 @@ impl DirectiveVisitor<'_> {
                             });
                         }
 
-                        if value == "use cache" {
+                        if rest.is_empty() {
                             self.directive = Some(Directive::UseCache {
                                 cache_kind: rcstr!("default"),
                             });
+
                             self.increment_cache_usage_counter("default");
+
+                            return true;
+                        }
+
+                        if rest.starts_with(": ") {
+                            let cache_kind = RcStr::from(rest.split_at(": ".len()).1);
+
+                            if !cache_kind.is_empty() {
+                                if !self.config.cache_kinds.contains(&cache_kind) {
+                                    emit_error(ServerActionsErrorKind::UnknownCacheKind {
+                                        span: *span,
+                                        cache_kind: cache_kind.clone(),
+                                    });
+                                }
+
+                                self.increment_cache_usage_counter(&cache_kind);
+                                self.directive = Some(Directive::UseCache { cache_kind });
+
+                                return true;
+                            }
+                        }
+
+                        // Emit helpful errors for variants like "use cache:<cache-kind>",
+                        // "use cache : <cache-kind>", and "use cache <cache-kind>" etc.
+                        let expected_directive = if let Some(colon_pos) = rest.find(':') {
+                            let kind = rest[colon_pos + 1..].trim();
+
+                            if kind.is_empty() {
+                                "use cache: <cache-kind>".to_string()
+                            } else {
+                                format!("use cache: {kind}")
+                            }
                         } else {
-                            // Slice the value after "use cache: "
-                            let cache_kind = RcStr::from(value.split_at("use cache: ".len()).1);
-
-                            if !self.config.cache_kinds.contains(&cache_kind) {
-                                emit_error(ServerActionsErrorKind::UnknownCacheKind {
-                                    span: *span,
-                                    cache_kind: cache_kind.clone(),
-                                });
+                            let kind = rest.trim();
+
+                            if kind.is_empty() {
+                                "use cache".to_string()
+                            } else {
+                                format!("use cache: {kind}")
                             }
+                        };
 
-                            self.increment_cache_usage_counter(&cache_kind);
-                            self.directive = Some(Directive::UseCache { cache_kind });
-                        }
+                        emit_error(ServerActionsErrorKind::MisspelledDirective {
+                            span: *span,
+                            directive: value.to_string(),
+                            expected_directive,
+                        });
 
                         return true;
                     } else {

filename: crates/next-custom-transforms/tests/errors/server-actions/server-graph/28/input.js
patch: @@ -0,0 +1,25 @@
+'use cache:remote'
+
+export async function foo() {
+  return 'data'
+}
+
+export async function bar() {
+  'use cache : default'
+  return 'data'
+}
+
+export async function baz() {
+  'use cache private'
+  return 'data'
+}
+
+export async function qux() {
+  'use cache '
+  return 'data'
+}
+
+export async function quux() {
+  'use cache: '
+  return 'data'
+}

filename: crates/next-custom-transforms/tests/errors/server-actions/server-graph/28/output.js
patch: @@ -0,0 +1,15 @@
+export async function foo() {
+    return 'data';
+}
+export async function bar() {
+    return 'data';
+}
+export async function baz() {
+    return 'data';
+}
+export async function qux() {
+    return 'data';
+}
+export async function quux() {
+    return 'data';
+}

filename: crates/next-custom-transforms/tests/errors/server-actions/server-graph/28/output.stderr
patch: @@ -0,0 +1,38 @@
+  x Did you mean "use cache: remote"? "use cache:remote" is not a supported directive name."
+  | 
+   ,-[input.js:1:1]
+ 1 | 'use cache:remote'
+   : ^^^^^^^^^^^^^^^^^^
+   `----
+  x Did you mean "use cache: default"? "use cache : default" is not a supported directive name."
+  | 
+   ,-[input.js:8:1]
+ 7 | export async function bar() {
+ 8 |   'use cache : default'
+   :   ^^^^^^^^^^^^^^^^^^^^^
+ 9 |   return 'data'
+   `----
+  x Did you mean "use cache: private"? "use cache private" is not a supported directive name."
+  | 
+    ,-[input.js:13:1]
+ 12 | export async function baz() {
+ 13 |   'use cache private'
+    :   ^^^^^^^^^^^^^^^^^^^
+ 14 |   return 'data'
+    `----
+  x Did you mean "use cache"? "use cache " is not a supported directive name."
+  | 
+    ,-[input.js:18:1]
+ 17 | export async function qux() {
+ 18 |   'use cache '
+    :   ^^^^^^^^^^^^
+ 19 |   return 'data'
+    `----
+  x Did you mean "use cache: <cache-kind>"? "use cache: " is not a supported directive name."
+  | 
+    ,-[input.js:23:1]
+ 22 | export async function quux() {
+ 23 |   'use cache: '
+    :   ^^^^^^^^^^^^^
+ 24 |   return 'data'
+    `----