diff --git a/.changeset/silly-chairs-rest.md b/.changeset/silly-chairs-rest.md new file mode 100644 index 0000000000..b56276eaf8 --- /dev/null +++ b/.changeset/silly-chairs-rest.md @@ -0,0 +1,5 @@ +--- +'@workflow/nitro': patch +--- + +Skip Workflow transforms for generated Nitro build artifacts. diff --git a/packages/nitro/src/index.test.ts b/packages/nitro/src/index.test.ts index ca9ecc8d35..4a722986ef 100644 --- a/packages/nitro/src/index.test.ts +++ b/packages/nitro/src/index.test.ts @@ -152,6 +152,56 @@ describe('@workflow/nitro builder lifecycle', () => { }); }); +describe('@workflow/nitro transform boundaries', () => { + it('does not re-transform generated Nitro build artifacts', async () => { + const rollupBeforeHooks: Array<(nitro: any, config: any) => void> = []; + const nitro = createNitroStub({ routing: true }); + nitro.hooks.hook = ( + name: string, + hook: (nitro: any, config: any) => void + ) => { + if (name === 'rollup:before') rollupBeforeHooks.push(hook); + }; + + const plugins = viteWorkflow(); + const viteTransform = plugins.find( + (plugin) => plugin.name === 'workflow:transform' + ) as any; + const viteNitro = plugins.find( + (plugin) => plugin.name === 'workflow:nitro' + ) as any; + + await viteNitro.nitro.setup(nitro); + + const config: { plugins: any[] } = { plugins: [] }; + for (const hook of rollupBeforeHooks) { + hook(nitro, config); + } + const nitroTransform = config.plugins.find( + (plugin: { name?: string }) => plugin.name === 'workflow:transform' + ); + + const code = ` + import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde'; + export class Serializable { + static [WORKFLOW_SERIALIZE](value) { return value; } + static [WORKFLOW_DESERIALIZE]() { return new Serializable(); } + } + `; + const generatedId = '/tmp/.nitro/vite/services/ssr/assets/index.js'; + const siblingId = '/tmp/.nitro-source/index.js'; + + for (const transform of [viteTransform, nitroTransform]) { + await expect( + transform.transform.call({}, code, generatedId) + ).resolves.toBeNull(); + await expect( + transform.transform.call({}, code, siblingId) + ).resolves.not.toBeNull(); + } + }); +}); + describe('@workflow/nitro world target bundling', () => { it('forces workflow SDK packages inline in production builds so aliases can resolve', async () => { const rollupBeforeHooks: Array<(nitro: any, config: any) => void> = []; diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index 15f3e161bc..f6a00b5d53 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -180,9 +180,7 @@ export default { const isVercelDeploy = !nitro.options.dev && nitro.options.preset === 'vercel'; const workflowTargetWorld = ensureWorkflowTargetWorldEnv(); - - // Pre-built workflow bundles directory - must be excluded from re-transformation - const workflowBuildDir = join(nitro.options.buildDir, 'workflow'); + const nitroBuildDir = `${nitro.options.buildDir.replace(/[\\/]+$/, '')}/`; nitro.options.alias[WORKFLOW_WORLD_TARGET_MODULE] = resolveWorkflowTargetWorldAlias(nitro, workflowTargetWorld); @@ -193,10 +191,9 @@ export default { nitro.hooks.hook('rollup:before', (_nitro: Nitro, config: RollupConfig) => { (config.plugins as Array).unshift( workflowTransformPlugin({ - // Exclude pre-built workflow bundles from re-transformation - // These are already processed and re-processing causes issues like - // undefined class references when Nitro's bundler renames variables - exclude: [workflowBuildDir], + // Nitro build artifacts have already been transformed. + // Re-processing them can duplicate class registration. + exclude: [nitroBuildDir], }) ); diff --git a/packages/nitro/src/vite.ts b/packages/nitro/src/vite.ts index 6a6116ba18..2c9ba7d334 100644 --- a/packages/nitro/src/vite.ts +++ b/packages/nitro/src/vite.ts @@ -9,7 +9,6 @@ import { workflowTransformPlugin } from '@workflow/rollup'; import { workflowHotUpdatePlugin } from '@workflow/vite'; import type { Nitro } from 'nitro/types'; import type {} from 'nitro/vite'; -import { join } from 'pathe'; import type { Plugin, TransformResult } from 'vite'; import { LocalBuilder } from './builders.js'; import type { ModuleOptions } from './index.js'; @@ -18,10 +17,10 @@ import nitroModule from './index.js'; export function workflow(options?: ModuleOptions): Plugin[] { let builder: LocalBuilder; let devNitro: Nitro | undefined; - let workflowBuildDir: string; + let nitroBuildDir: string; const enqueue = createBuildQueue(); - // Create a lazy transform plugin that excludes the workflow build directory + // Create a lazy transform plugin that excludes Nitro build artifacts. // The exclusion path is set during nitro setup, so we need to defer plugin creation const lazyTransformPlugin: Plugin = { name: 'workflow:transform', @@ -57,9 +56,9 @@ export function workflow(options?: ModuleOptions): Plugin[] { }, transform(code, id, options) { // Delegate to the actual transform plugin with exclusion - // workflowBuildDir is set during nitro setup before transforms run + // nitroBuildDir is set during nitro setup before transforms run const plugin = workflowTransformPlugin({ - exclude: workflowBuildDir ? [workflowBuildDir] : [], + exclude: nitroBuildDir ? [nitroBuildDir] : [], }); const transform = plugin.transform as | (( @@ -79,8 +78,8 @@ export function workflow(options?: ModuleOptions): Plugin[] { name: 'workflow:nitro', nitro: { setup: (nitro: Nitro) => { - // Capture the workflow build directory for exclusion - workflowBuildDir = join(nitro.options.buildDir, 'workflow'); + // Capture the Nitro build directory for exclusion + nitroBuildDir = `${nitro.options.buildDir.replace(/[\\/]+$/, '')}/`; nitro.options.workflow = { ...nitro.options.workflow, ...options,