Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8345559
Setup fixes
ijjk Feb 28, 2026
e710df7
ci: run local e2e against staged tarball workbenches
ijjk Mar 1, 2026
d324048
ci: update staged workbench tarball setup script
ijjk Mar 1, 2026
a52553d
chore: set nextjs workbenches back to next 16.1.6
ijjk Mar 1, 2026
09577bc
update lock
ijjk Mar 1, 2026
9aa25f9
test(e2e): resolve workbench path from WORKBENCH_APP_PATH
ijjk Mar 1, 2026
8b0e4e4
fix: address deferred builder issues outside monorepo
ijjk Mar 1, 2026
4b46493
ci: stage tarball workbenches only for nextjs local e2e
ijjk Mar 1, 2026
440502e
fix(next): discover deferred steps imported via workflows
ijjk Mar 1, 2026
701640a
test(core): gate deferred step-discovery dev test to canary
ijjk Mar 1, 2026
75906d3
test(e2e): cover cross-file imported step in build/start lanes
ijjk Mar 1, 2026
5788877
fix(e2e): use local manifest in local runs and relax dev rebuild timeout
ijjk Mar 1, 2026
dfc51c2
fix(workbench): add imported-step workflow symlink for sveltekit/astro
ijjk Mar 1, 2026
78a4ad5
test(e2e): scope imported-step workflow test to nextjs lanes
ijjk Mar 1, 2026
2c3e920
fix(next): rebuild deferred entries on discovered file updates
ijjk Mar 2, 2026
cddd3c5
fix(next): watch transitive deferred step deps for dev rebuilds
ijjk Mar 2, 2026
248f5a9
fix(next): restore socket-driven deferred step rebuilds
ijjk Mar 2, 2026
0c5c0ea
Merge branch 'main' into ijjk/fix-lazy-test-handling
ijjk Mar 2, 2026
e4180da
add changeset
ijjk Mar 2, 2026
0ebec42
chore: address review feedback on deferred e2e updates
ijjk Mar 3, 2026
a783ad6
fix(cli): guard stream flush against closed write streams
ijjk Mar 3, 2026
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
8 changes: 8 additions & 0 deletions .changeset/thick-beers-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@workflow/builders": patch
"@workflow/core": patch
"@workflow/next": patch
"@workflow/cli": patch
---

Fix deferred build mode for Next.js
33 changes: 33 additions & 0 deletions .github/actions/prepare-workbench-path/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: 'Prepare Workbench Path'
description: 'Resolve a workbench path, staging tarball-based Next.js workbenches when needed.'

inputs:
app-name:
description: 'Workbench app name from test matrix'
required: true

outputs:
workbench_app_path:
description: 'Resolved absolute path to the workbench used by tests'
value: ${{ steps.prepare.outputs.workbench_app_path }}

runs:
using: 'composite'
steps:
- id: prepare
shell: bash
run: |
if [[ "${{ inputs.app-name }}" == "nextjs-turbopack" || "${{ inputs.app-name }}" == "nextjs-webpack" ]]; then
STAGE_LOG="$(mktemp)"
node scripts/stage-workbench-with-tarballs.mjs "workbench/${{ inputs.app-name }}" | tee "$STAGE_LOG"
WORKBENCH_APP_PATH="$(sed -n 's/^Staged workbench: //p' "$STAGE_LOG" | tail -n 1)"
if [ -z "$WORKBENCH_APP_PATH" ]; then
echo "Failed to parse staged workbench path from stage-workbench-with-tarballs output"
exit 1
fi
else
./scripts/resolve-symlinks.sh "workbench/${{ inputs.app-name }}"
WORKBENCH_APP_PATH="$GITHUB_WORKSPACE/workbench/${{ inputs.app-name }}"
fi

echo "workbench_app_path=$WORKBENCH_APP_PATH" >> "$GITHUB_OUTPUT"
32 changes: 23 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,18 +345,22 @@ jobs:
- name: Run Initial Build
run: pnpm turbo run build --filter='!./workbench/*'

- name: Resolve symlinks
run: ./scripts/resolve-symlinks.sh workbench/${{ matrix.app.name }}
- name: Prepare workbench path
Comment thread
ijjk marked this conversation as resolved.
id: prepare-workbench
uses: ./.github/actions/prepare-workbench-path
with:
app-name: ${{ matrix.app.name }}

- name: Run E2E Tests
run: |
cd workbench/${{ matrix.app.name }} && pnpm dev &
cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && pnpm dev &
echo "starting tests in 10 seconds" && sleep 10
pnpm vitest run packages/core/e2e/dev.test.ts; sleep 10
pnpm run test:e2e --reporter=default --reporter=json --outputFile=e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json
env:
NODE_OPTIONS: "--enable-source-maps"
APP_NAME: ${{ matrix.app.name }}
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '5173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"
DEV_TEST_CONFIG: ${{ toJSON(matrix.app) }}

Expand Down Expand Up @@ -413,22 +417,27 @@ jobs:
- name: Run Initial Build
run: pnpm turbo run build --filter='!./workbench/*'

- name: Resolve symlinks
run: ./scripts/resolve-symlinks.sh workbench/${{ matrix.app.name }}
- name: Prepare workbench path
id: prepare-workbench
uses: ./.github/actions/prepare-workbench-path
with:
app-name: ${{ matrix.app.name }}

- name: Run Build Tests
run: pnpm vitest run packages/core/e2e/local-build.test.ts
env:
APP_NAME: ${{ matrix.app.name }}
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}

- name: Run E2E Tests
run: |
cd workbench/${{ matrix.app.name }} && pnpm start &
cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && pnpm start &
echo "starting tests in 10 seconds" && sleep 10
pnpm run test:e2e --reporter=default --reporter=json --outputFile=e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json
env:
NODE_OPTIONS: "--enable-source-maps"
APP_NAME: ${{ matrix.app.name }}
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '4173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"

- name: Generate E2E summary
Expand Down Expand Up @@ -504,22 +513,27 @@ jobs:
- name: Setup PostgreSQL Database
run: ./packages/world-postgres/bin/setup.js

- name: Resolve symlinks
run: ./scripts/resolve-symlinks.sh workbench/${{ matrix.app.name }}
- name: Prepare workbench path
id: prepare-workbench
uses: ./.github/actions/prepare-workbench-path
with:
app-name: ${{ matrix.app.name }}

- name: Run Build Tests
run: pnpm vitest run packages/core/e2e/local-build.test.ts
env:
APP_NAME: ${{ matrix.app.name }}
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}

- name: Run E2E Tests
run: |
cd workbench/${{ matrix.app.name }} && pnpm start &
cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && pnpm start &
echo "starting tests in 10 seconds" && sleep 10
pnpm run test:e2e --reporter=default --reporter=json --outputFile=e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json
env:
NODE_OPTIONS: "--enable-source-maps"
APP_NAME: ${{ matrix.app.name }}
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '4173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"

- name: Generate E2E summary
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"clean": "turbo clean",
"typecheck": "turbo typecheck",
"test:e2e": "vitest run packages/core/e2e/e2e.test.ts",
"test:e2e:nextjs-webpack:staged": "node scripts/test-staged-nextjs-webpack.mjs",
"test:docs": "pnpm --filter @workflow/docs-typecheck test:docs",
"bench": "vitest bench packages/core/e2e/bench.bench.ts",
"bench:local": "DEPLOYMENT_URL=http://localhost:3000 APP_NAME=nextjs-turbopack vitest bench packages/core/e2e/bench.bench.ts",
Expand All @@ -42,7 +43,8 @@
"changeset": "changeset",
"ci:version": "changeset version",
"ci:publish": "pnpm build && changeset publish",
"release:notes": "node scripts/generate-release-notes.mjs"
"release:notes": "node scripts/generate-release-notes.mjs",
"workbench:stage": "node scripts/stage-workbench-with-tarballs.mjs"
},
"lint-staged": {
"**/*": "biome format --write --no-errors-on-unmatched"
Expand Down
52 changes: 52 additions & 0 deletions packages/builders/src/module-specifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,56 @@ describe('getImportPath', () => {
isPackage: true,
});
});

it('uses package subpath import for direct node_modules dependencies', () => {
const projectRoot = join(testRoot, 'apps/chat');
const packageDir = join(testRoot, 'apps/chat/node_modules/@workflow/core');
const filePath = join(packageDir, 'dist/serialization.js');

writeJson(join(projectRoot, 'package.json'), {
name: 'chat',
dependencies: { '@workflow/core': '1.0.0' },
});

writeJson(join(packageDir, 'package.json'), {
name: '@workflow/core',
version: '1.0.0',
exports: {
'./serialization': './dist/serialization.js',
},
});

writeFile(filePath, `'use workflow';\n`);

expect(getImportPath(filePath, projectRoot)).toEqual({
importPath: '@workflow/core/serialization',
isPackage: true,
});
});

it('falls back to relative import for transitive node_modules dependencies', () => {
const projectRoot = join(testRoot, 'apps/chat');
const packageDir = join(testRoot, 'apps/chat/node_modules/@workflow/core');
const filePath = join(packageDir, 'dist/serialization.js');

writeJson(join(projectRoot, 'package.json'), {
name: 'chat',
dependencies: { workflow: '1.0.0' },
});

writeJson(join(packageDir, 'package.json'), {
name: '@workflow/core',
version: '1.0.0',
exports: {
'./serialization': './dist/serialization.js',
},
});

writeFile(filePath, `'use workflow';\n`);

expect(getImportPath(filePath, projectRoot)).toEqual({
importPath: './node_modules/@workflow/core/dist/serialization.js',
isPackage: false,
});
});
});
15 changes: 15 additions & 0 deletions packages/builders/src/module-specifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,21 @@ export function getImportPath(
// Find the package.json for this file
Comment thread
ijjk marked this conversation as resolved.
const pkg = findPackageJson(filePath);
if (pkg) {
const isDirectProjectDependency = getProjectDependencies(projectRoot).has(
pkg.name
);
const canUsePackageSpecifier = inWorkspace || isDirectProjectDependency;

// For transitive node_modules dependencies under strict package managers
// (for example, pnpm), importing by package name can fail. Use a direct
// file path import in those cases.
if (!canUsePackageSpecifier) {
return {
importPath: toRelativeImportPath(filePath, projectRoot),
isPackage: false,
};
}

// Prefer a package subpath import when this file maps to an export.
// This preserves the exact module being bundled while still respecting
// package export conditions.
Expand Down
41 changes: 41 additions & 0 deletions packages/builders/src/transform-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
detectWorkflowPatterns,
isWorkflowSdkFile,
useStepPattern,
useWorkflowPattern,
workflowSerdeComputedPropertyPattern,
Expand Down Expand Up @@ -332,4 +333,44 @@ describe('transform-utils patterns', () => {
expect(result.hasSerde).toBe(true);
});
});

describe('isWorkflowSdkFile', () => {
it('matches direct @workflow package path in node_modules', () => {
expect(
isWorkflowSdkFile(
'/tmp/app/node_modules/@workflow/core/dist/serialization.js'
)
).toBe(true);
});

it('matches direct workflow package path in node_modules', () => {
expect(
isWorkflowSdkFile('/tmp/app/node_modules/workflow/dist/runtime.js')
).toBe(true);
});

it('matches pnpm virtual store @workflow package path', () => {
expect(
isWorkflowSdkFile(
'/tmp/app/node_modules/.pnpm/@workflow+core@4.1.0/node_modules/@workflow/core/dist/serialization.js'
)
).toBe(true);
});

it('matches pnpm virtual store workflow package path', () => {
expect(
isWorkflowSdkFile(
'/tmp/app/node_modules/.pnpm/workflow@4.1.0/node_modules/workflow/dist/runtime.js'
)
).toBe(true);
});

it('does not match non-workflow package in pnpm store', () => {
expect(
isWorkflowSdkFile(
'/tmp/app/node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/lodash.js'
)
).toBe(false);
});
});
});
9 changes: 7 additions & 2 deletions packages/builders/src/transform-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,15 @@ export const generatedWorkflowPathPattern =

// Pattern to detect @workflow SDK packages that should be excluded from transformation
// These are internal SDK packages that should not be treated as user entry points.
// Matches both: node_modules/@workflow/* and monorepo packages/*/dist paths
// Matches:
// - node_modules/@workflow/*
// - node_modules/workflow/*
// - node_modules/.pnpm/.../node_modules/@workflow/* (pnpm virtual store)
// - node_modules/.pnpm/.../node_modules/workflow/* (pnpm virtual store)
// - monorepo packages/*/dist paths
// User npm packages with workflows/steps/serde SHOULD still be discovered.
export const workflowSdkPathPattern =
/[/\\](?:node_modules[/\\]@workflow[/\\]|packages[/\\](?:builders|core|rollup|vite|next|nitro|serde|workflow|swc-plugin-workflow)[/\\])/;
/[/\\](?:node_modules[/\\](?:@workflow[/\\]|workflow[/\\]|\.pnpm[/\\][^/\\]+[/\\]node_modules[/\\](?:@workflow[/\\]|workflow[/\\]))|packages[/\\](?:builders|core|rollup|vite|next|nitro|serde|workflow|swc-plugin-workflow)[/\\])/;

/**
* Detects workflow-related patterns in source code.
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import { Command } from '@oclif/core';
import { getWorld } from '@workflow/core/runtime';

async function flushStream(stream: NodeJS.WriteStream): Promise<void> {
if (
!stream.writable ||
stream.destroyed ||
stream.closed ||
stream.writableEnded ||
stream.writableFinished
) {
return;
}

await new Promise<void>((resolve) => {
const onError = () => resolve();
stream.once('error', onError);
try {
stream.write('', () => {
stream.off('error', onError);
resolve();
});
} catch {
stream.off('error', onError);
resolve();
}
});
Comment thread
ijjk marked this conversation as resolved.
}

export abstract class BaseCommand extends Command {
static enableJsonFlag = true;

Expand All @@ -23,6 +49,10 @@ export abstract class BaseCommand extends Command {
// agents, but third-party libraries (oclif update checker, postgres.js)
// may leave timers or sockets that prevent the event loop from draining.
// This is safe because all business logic and cleanup has completed.
await Promise.all([
flushStream(process.stdout),
flushStream(process.stderr),
]);
process.exit(err ? 1 : 0);
}

Expand Down
Loading