diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 0a3837780..cdc29f6e4 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1,6 +1,5 @@ import { NextBuilder } from './builder.js'; import type { NextConfig } from 'next'; -import semver from 'semver'; export function withWorkflow( nextConfigOrFn: @@ -51,35 +50,10 @@ export function withWorkflow( // shallow clone to avoid read-only on top-level nextConfig = Object.assign({}, nextConfig); - // configure the loader if turbopack is being used - if (!nextConfig.turbopack) { - nextConfig.turbopack = {}; - } - if (!nextConfig.turbopack.rules) { - nextConfig.turbopack.rules = {}; - } - const existingRules = nextConfig.turbopack.rules as any; - const nextVersion = require('next/package.json').version; - const supportsTurboCondition = semver.gte(nextVersion, 'v16.0.0'); - - for (const key of ['*.tsx', '*.ts', '*.jsx', '*.js']) { - nextConfig.turbopack.rules[key] = { - ...(supportsTurboCondition - ? { - condition: { - ...existingRules[key]?.condition, - any: [ - ...(existingRules[key]?.condition.any || []), - { - content: /(use workflow|use step)/, - }, - ], - }, - } - : {}), - loaders: [...(existingRules[key]?.loaders || []), loaderPath], - }; - } + // NOTE: Turbopack configuration is currently disabled due to conflicts with Next.js's + // built-in TypeScript and JSX transform handling. The webpack loader below handles + // workflow transformations for both webpack and turbopack builds. + // TODO: Investigate proper Turbopack loader integration that doesn't override defaults. // configure the loader for webpack const existingWebpackModify = nextConfig.webpack; diff --git a/packages/next/src/loader.ts b/packages/next/src/loader.ts index b1ffb6788..b0daeb6ba 100644 --- a/packages/next/src/loader.ts +++ b/packages/next/src/loader.ts @@ -1,85 +1,123 @@ import { relative } from 'node:path'; import { transform } from '@swc/core'; +// Minimal webpack loader context type +interface LoaderContext { + resourcePath: string; + async(): ( + err: Error | null, + content?: string | Buffer, + sourceMap?: any + ) => void; +} + // This loader applies the "use workflow"/"use step" // client transformation -export default async function workflowLoader( - this: { - resourcePath: string; - }, +export default function workflowLoader( + this: LoaderContext, source: string | Buffer, sourceMap: any -): Promise { +) { + const callback = this.async(); + if (!callback) { + throw new Error('Workflow loader requires async support'); + } + const filename = this.resourcePath; const normalizedSource = source.toString(); // only apply the transform if file needs it if (!normalizedSource.match(/(use step|use workflow)/)) { - return normalizedSource; + // Pass through to the next loader without transformation + return callback(null, source, sourceMap); } - const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx'); - const isTsx = filename.endsWith('.tsx'); + processWorkflowFile.call( + this, + normalizedSource, + filename, + sourceMap, + callback + ); +} + +async function processWorkflowFile( + this: LoaderContext, + normalizedSource: string, + filename: string, + sourceMap: any, + callback: ( + err: Error | null, + content?: string | Buffer, + sourceMap?: any + ) => void +) { + try { + const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx'); + const isTsx = filename.endsWith('.tsx'); - // Calculate relative filename for SWC plugin - // The SWC plugin uses filename to generate workflowId, so it must be relative - const workingDir = process.cwd(); - const normalizedWorkingDir = workingDir - .replace(/\\/g, '/') - .replace(/\/$/, ''); - const normalizedFilepath = filename.replace(/\\/g, '/'); + // Calculate relative filename for SWC plugin + // The SWC plugin uses filename to generate workflowId, so it must be relative + const workingDir = process.cwd(); + const normalizedWorkingDir = workingDir + .replace(/\\/g, '/') + .replace(/\/$/, ''); + const normalizedFilepath = filename.replace(/\\/g, '/'); - // Windows fix: Use case-insensitive comparison to work around drive letter casing issues - const lowerWd = normalizedWorkingDir.toLowerCase(); - const lowerPath = normalizedFilepath.toLowerCase(); + // Windows fix: Use case-insensitive comparison to work around drive letter casing issues + const lowerWd = normalizedWorkingDir.toLowerCase(); + const lowerPath = normalizedFilepath.toLowerCase(); - let relativeFilename: string; - if (lowerPath.startsWith(lowerWd + '/')) { - // File is under working directory - manually calculate relative path - relativeFilename = normalizedFilepath.substring( - normalizedWorkingDir.length + 1 - ); - } else if (lowerPath === lowerWd) { - // File IS the working directory (shouldn't happen) - relativeFilename = '.'; - } else { - // Use relative() for files outside working directory - relativeFilename = relative(workingDir, filename).replace(/\\/g, '/'); + let relativeFilename: string; + if (lowerPath.startsWith(lowerWd + '/')) { + // File is under working directory - manually calculate relative path + relativeFilename = normalizedFilepath.substring( + normalizedWorkingDir.length + 1 + ); + } else if (lowerPath === lowerWd) { + // File IS the working directory (shouldn't happen) + relativeFilename = '.'; + } else { + // Use relative() for files outside working directory + relativeFilename = relative(workingDir, filename).replace(/\\/g, '/'); - if (relativeFilename.startsWith('../')) { - relativeFilename = relativeFilename - .split('/') - .filter((part) => part !== '..') - .join('/'); + if (relativeFilename.startsWith('../')) { + relativeFilename = relativeFilename + .split('/') + .filter((part) => part !== '..') + .join('/'); + } } - } - // Final safety check - ensure we never pass an absolute path to SWC - if (relativeFilename.includes(':') || relativeFilename.startsWith('/')) { - // This should rarely happen, but use filename split as last resort - relativeFilename = normalizedFilepath.split('/').pop() || 'unknown.ts'; - } + // Final safety check - ensure we never pass an absolute path to SWC + if (relativeFilename.includes(':') || relativeFilename.startsWith('/')) { + // This should rarely happen, but use filename split as last resort + relativeFilename = normalizedFilepath.split('/').pop() || 'unknown.ts'; + } - // Transform with SWC - const result = await transform(normalizedSource, { - filename: relativeFilename, - jsc: { - parser: { - syntax: isTypeScript ? 'typescript' : 'ecmascript', - tsx: isTsx, + // Transform with SWC + const result = await transform(normalizedSource, { + filename: relativeFilename, + jsc: { + parser: { + syntax: isTypeScript ? 'typescript' : 'ecmascript', + tsx: isTsx, + }, + target: 'es2022', + experimental: { + plugins: [ + [require.resolve('@workflow/swc-plugin'), { mode: 'client' }], + ], + }, }, - target: 'es2022', - experimental: { - plugins: [ - [require.resolve('@workflow/swc-plugin'), { mode: 'client' }], - ], - }, - }, - minify: false, - inputSourceMap: sourceMap, - sourceMaps: true, - inlineSourcesContent: true, - }); + minify: false, + inputSourceMap: sourceMap, + sourceMaps: true, + inlineSourcesContent: true, + }); - return result.code; + callback(null, result.code, result.map); + } catch (error) { + callback(error as Error); + } }