turbopack: cache TransformPlugin in narrow-scoped turbo-tasks functions (#92842)
### What?
Refactor all usages of
`EcmascriptInputTransform::Plugin(ResolvedVc::cell(Box::new(...) as _))`
across `next-core` and `turbopack-tests` so that `TransformPlugin` cells
are created inside dedicated `#[turbo_tasks::function]` functions rather
than inline at call sites.
Additionally:
- Introduce a `JsonValue` newtype wrapping `serde_json::Value` that
implements `TaskInput`, enabling the SWC wasm plugin list to be passed
through a turbo-tasks function boundary and properly cached.
- Replace the `bool` roundtrip in `next_strip_page_exports` with an
`ExportFilterInput` enum that derives `TaskInput`, mirroring
`ExportFilter` exhaustively (so a new upstream variant is a compile
error, not a silent fallback).
- Derive `TaskInput` on `ActionsTransform` and pass it directly to the
cached function instead of converting to `is_server: bool` at the call
site.
- Replace all `.expect("... config must exist")` panics in option-gated
plugin functions (`emotion`, `styled_components`,
`react_remove_properties`, `remove_console`, `relay`) with
`.context(...)?` for proper error propagation.
- Add `// TODO: use get_ecma_transform_rule instead` comments to the ~10
transform functions that manually inline the same `ModuleRule::new` +
`ExtendEcmascriptTransforms` pattern that `get_ecma_transform_rule`
abstracts.
### Why?
`TransformPlugin` is not serializable and not comparable. When a
`TransformPlugin` is `cell`ed inline (i.e.
`ResolvedVc::cell(Box::new(...) as _)`) inside a turbo-tasks function, a
new cell is created on every invocation of the enclosing function,
because the framework has no way to detect that the value is the same as
before. This causes every task that depends on the `TransformPlugin`
cell to be invalidated unnecessarily.
By moving the `Vc::cell(...)` call into its own narrow-scoped
`#[turbo_tasks::function]`, turbo-tasks can cache the cell by the
function's inputs. If the inputs haven't changed, the function won't
re-run, the existing cell is reused, and downstream tasks are not
invalidated.
### How?
Each inline `ResolvedVc::cell(Box::new(SomeTransformer { ... }) as _)`
is replaced with:
```rust
some_transform_plugin(args).to_resolved().await?
#[turbo_tasks::function]
fn some_transform_plugin(args: ...) -> Vc<TransformPlugin> {
Vc::cell(Box::new(SomeTransformer { ... }) as Box<dyn CustomTransformer + Send + Sync>)
}
```
Where a cached function needs to store a `ResolvedVc` in the resulting
transformer struct, the parameter is declared as `ResolvedVc<T>`
directly in the `#[turbo_tasks::function]` signature. The turbo_tasks
macro rewrites `ResolvedVc<T>` → `Vc<T>` in the external call-site
signature, and the call site passes a dereferenced `*resolved_vc`. This
avoids a redundant `.to_resolved().await?` inside the function body.
For the SWC wasm plugin case, `serde_json::Value` (used for per-plugin
config) doesn't implement `Hash` or `TaskInput`. A `JsonValue` newtype
is introduced with:
- `#[bincode(with = "turbo_bincode::serde_self_describing")]` for
serialization
- Manual `Hash` impl that hashes the JSON string representation
- Manual `TaskInput` impl (`is_transient = false` since the type
contains no `Vc`s)
<!-- NEXT_JS_LLM_PR -->
---------
Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Niklas Mischkulnig <mischnic@users.noreply.github.com>