Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 4 additions & 30 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { NextBuilder } from './builder.js';
import type { NextConfig } from 'next';
import semver from 'semver';

export function withWorkflow(
nextConfigOrFn:
Expand Down Expand Up @@ -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;
Expand Down
160 changes: 99 additions & 61 deletions packages/next/src/loader.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
) {
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);
}
}