Skip to content

Decorators in a workspace package fail to parse after 4.5.0 (SWC transform failed: Expression expected) #3356

Description

@caleblehman-robosource

Summary

In a pnpm workspace monorepo, upgrading workflow from 4.5.0 to any later 4.x release makes the SWC workflow plugin fail to parse TypeScript decorators in a workspace package that the Next.js app depends on. Every decorated class in that package fails with:

SWC transform failed:   x Expression expected
    ,-[lib/shared/src/services/taskService.ts:46:1]
 46 | @processCoachInjectable()
    : ^
Caused by:
    Syntax Error

experimentalDecorators: true is set in every relevant tsconfig.json, and the same build is clean on 4.5.0.

Versions

workflow @workflow/builders web build
4.5.0 4.1.0 pass, 0 errors
4.6.0 4.1.1 fail, 41 errors
4.6.2 4.1.3 fail, 41 errors
4.7.0 4.1.4 fail, 41 errors
4.8.0 4.1.5 fail, 41 errors

Next.js 16, pnpm 10.26.2, Node 22.22.2, macOS. Same result in CI on Linux.

Setup

repo-root/
  pnpm-workspace.yaml          <- no tsconfig.json at this level
  web/                         <- Next.js app, withWorkflow(nextConfig)
    tsconfig.json              <- experimentalDecorators: true
  lib/shared/                  <- workspace package "@lib/shared"
    tsconfig.json              <- experimentalDecorators: true, emitDecoratorMetadata: true
    src/…                      <- ~150 classes decorated for a tsyringe DI container

web/tsconfig.json maps @lib/shared/workflows/* to ../lib/shared/src/workflows/* so the compiler sees "use step" / "use workflow" directives in source. Workflow steps reach the DI container, which lazily loads the service graph with a dynamic import(). On 4.6+ the compiler follows that dynamic import into the workspace package's TypeScript source and then fails to parse the decorators there.

What I ruled out

Not a missing experimentalDecorators. All three tsconfigs set it. Suspecting the new workspace-root resolution meant findUp landed somewhere without a tsconfig, I added a root tsconfig.json with experimentalDecorators and emitDecoratorMetadata. Still 41 errors — including after deleting web/.next to rule out a cached transform.

Not user-configurable. applySwcTransform takes projectRoot and moduleSpecifierRoot, and swc-esbuild-plugin reads options.projectRoot, but workflow/next never passes either — grepping the shipped dist for those names returns nothing. withWorkflow's documented options are workflows.local.port only.

Suspected cause

Diffing @workflow/builders 4.1.0 against 4.1.5, the change that lines up is the new resolveProjectRoot() in config-helpers.js, which walks up to the workspace root by looking for pnpm-workspace.yaml (then lockfiles), together with the split of the old single projectRoot into projectRoot (tsconfig lookup) and moduleSpecifierRoot (package resolution).

The consequence for a monorepo: files in lib/shared/src used to be project-local relative to web/, and are now resolved as a workspace package. isProjectLocalFile(path, moduleSpecifierRoot) returns false for them where it previously returned true. I did not chase exactly which branch then drops the decorator parser options, but the version boundary and the diff both point here.

Workaround

Route the workspace package's internals through its exports map so the compiler sees compiled dist JavaScript, which has no decorators left to parse. That clears all 41 errors, but within a single compilation the src and dist copies of a class are distinct nominal types, so every boundary where they meet needs its types realigned — impractical past a handful of files.

Ask

Either restore the pre-4.6 project-root default, or expose projectRoot / moduleSpecifierRoot through withWorkflow so a monorepo can opt out. If treating workspace packages as packages is intentional, documenting that decorated source in a workspace package must be consumed as compiled output would at least make the upgrade path discoverable — there is currently no migration note for 4.5 → 4.6 (the shipped docs/changelog/index.mdx reads ## 2026 / - TBD).

Happy to test a patch against the real repo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions