From f3dd7fb2c4962277a45b58fe081137c150139f39 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 15 Apr 2026 12:04:14 -0700 Subject: [PATCH 1/2] Add stable Next.js eager and lazy test coverage --- .changeset/witty-mails-smile.md | 5 + .github/scripts/generate-docs-data.js | 2 +- .github/workflows/tests.yml | 34 +++--- packages/core/e2e/dev.test.ts | 13 ++- packages/core/e2e/local-build.test.ts | 10 +- packages/core/e2e/utils.ts | 19 ++++ packages/next/src/builder.test.ts | 32 ++++++ packages/next/src/builder.ts | 18 ++- packages/next/src/index.test.ts | 42 +++++++ packages/next/src/index.ts | 14 ++- packages/next/src/loader.ts | 14 ++- scripts/create-test-matrix.mjs | 154 ++++++++++++++------------ 12 files changed, 256 insertions(+), 101 deletions(-) create mode 100644 .changeset/witty-mails-smile.md create mode 100644 packages/next/src/builder.test.ts diff --git a/.changeset/witty-mails-smile.md b/.changeset/witty-mails-smile.md new file mode 100644 index 0000000000..affff22cd9 --- /dev/null +++ b/.changeset/witty-mails-smile.md @@ -0,0 +1,5 @@ +--- +"@workflow/next": patch +--- + +Allow `WORKFLOW_NEXT_LAZY_DISCOVERY=0` to explicitly disable deferred Next.js discovery diff --git a/.github/scripts/generate-docs-data.js b/.github/scripts/generate-docs-data.js index 7adbab3cf2..b0ef918e75 100644 --- a/.github/scripts/generate-docs-data.js +++ b/.github/scripts/generate-docs-data.js @@ -144,7 +144,7 @@ function parseE2EResults(files) { // Extract framework from filename for detailed breakdown const basename = path.basename(file, '.json'); const frameworkMatch = basename.match( - /-(nextjs-turbopack|nextjs-webpack|nitro|nuxt|sveltekit|vite|hono|express|fastify|astro)(?:-(canary|stable))?$/ + /-(nextjs-turbopack|nextjs-webpack|nitro|nuxt|sveltekit|vite|hono|express|fastify|astro)(?:-(?:canary|stable(?:-lazy-discovery-(?:enabled|disabled))?))?$/ ); if (frameworkMatch) { const framework = frameworkMatch[1]; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 768e857a2f..064205b842 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -253,6 +253,7 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} WORKFLOW_PUBLIC_MANIFEST: '1' + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.canary != true && (matrix.app.name == 'nextjs-turbopack' || matrix.app.name == 'nextjs-webpack') && '0' || '' }} steps: - name: Checkout Repo uses: actions/checkout@v4 @@ -332,7 +333,7 @@ jobs: run: echo "matrix=$(node ./scripts/create-test-matrix.mjs)" >> $GITHUB_OUTPUT e2e-local-dev: - name: E2E Local Dev Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) + name: E2E Local Dev Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }}) runs-on: ubuntu-latest timeout-minutes: 30 if: ${{ !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }} @@ -345,6 +346,7 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} WORKFLOW_PUBLIC_MANIFEST: '1' + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.lazyDiscovery == false && '0' || matrix.app.lazyDiscovery == true && '1' || '' }} steps: - name: Checkout Repo @@ -382,7 +384,7 @@ jobs: 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 --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json env: NODE_OPTIONS: "--enable-source-maps" APP_NAME: ${{ matrix.app.name }} @@ -393,19 +395,19 @@ jobs: - name: Generate E2E summary if: always() - run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Dev (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true + run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Dev (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }} - path: e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + name: e2e-results-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }} + path: e2e-local-dev-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json retention-days: 7 if-no-files-found: ignore e2e-local-prod: - name: E2E Local Prod Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) + name: E2E Local Prod Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }}) runs-on: ubuntu-latest timeout-minutes: 30 if: ${{ !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }} @@ -418,6 +420,7 @@ jobs: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} WORKFLOW_PUBLIC_MANIFEST: '1' + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.lazyDiscovery == false && '0' || matrix.app.lazyDiscovery == true && '1' || '' }} steps: - name: Checkout Repo @@ -460,7 +463,7 @@ jobs: run: | 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 --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json env: NODE_OPTIONS: "--enable-source-maps" APP_NAME: ${{ matrix.app.name }} @@ -470,19 +473,19 @@ jobs: - name: Generate E2E summary if: always() - run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Prod (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true + run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Prod (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }} - path: e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + name: e2e-results-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }} + path: e2e-local-prod-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json retention-days: 7 if-no-files-found: ignore e2e-local-postgres: - name: E2E Local Postgres Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) + name: E2E Local Postgres Tests (${{ matrix.app.name }} - ${{ matrix.app.runLabel }}) runs-on: ubuntu-latest timeout-minutes: 30 if: ${{ !contains(github.event.pull_request.labels.*.name, 'workflow-server-test') }} @@ -512,6 +515,7 @@ jobs: WORKFLOW_PUBLIC_MANIFEST: '1' WORKFLOW_TARGET_WORLD: "@workflow/world-postgres" WORKFLOW_POSTGRES_URL: "postgres://world:world@localhost:5432/world" + WORKFLOW_NEXT_LAZY_DISCOVERY: ${{ matrix.app.lazyDiscovery == false && '0' || matrix.app.lazyDiscovery == true && '1' || '' }} steps: - name: Checkout Repo @@ -557,7 +561,7 @@ jobs: run: | 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 --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + pnpm run test:e2e --reporter=default --reporter=json --reporter=./packages/core/e2e/github-reporter.ts --outputFile=e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json env: NODE_OPTIONS: "--enable-source-maps" APP_NAME: ${{ matrix.app.name }} @@ -567,14 +571,14 @@ jobs: - name: Generate E2E summary if: always() - run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Postgres (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true + run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Postgres (${{ matrix.app.name }} - ${{ matrix.app.runLabel }})" >> $GITHUB_STEP_SUMMARY || true - name: Upload E2E results if: always() uses: actions/upload-artifact@v4 with: - name: e2e-results-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }} - path: e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.canary && 'canary' || 'stable' }}.json + name: e2e-results-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }} + path: e2e-local-postgres-${{ matrix.app.name }}-${{ matrix.app.artifactSuffix }}.json retention-days: 7 if-no-files-found: ignore diff --git a/packages/core/e2e/dev.test.ts b/packages/core/e2e/dev.test.ts index 6d89c2d8b2..277fdc5712 100644 --- a/packages/core/e2e/dev.test.ts +++ b/packages/core/e2e/dev.test.ts @@ -1,7 +1,10 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { afterEach, beforeAll, describe, expect, test } from 'vitest'; -import { getWorkbenchAppPath } from './utils'; +import { + getWorkbenchAppPath, + isNextLazyDiscoveryEnabledForTest, +} from './utils'; export interface DevTestConfig { generatedStepPath: string; @@ -44,9 +47,11 @@ export function createDevTests(config?: DevTestConfig) { ); const testWorkflowFile = finalConfig.testWorkflowFile ?? '3_streams.ts'; const workflowsDir = finalConfig.workflowsDir ?? 'workflows'; - const supportsDeferredStepCopies = generatedStep.includes( - path.join('.well-known', 'workflow', 'v1', 'step', 'route.js') - ); + const supportsDeferredStepCopies = + isNextLazyDiscoveryEnabledForTest() && + generatedStep.includes( + path.join('.well-known', 'workflow', 'v1', 'step', 'route.js') + ); const restoreFiles: Array<{ path: string; content: string }> = []; const fetchWithTimeout = (pathname: string) => { diff --git a/packages/core/e2e/local-build.test.ts b/packages/core/e2e/local-build.test.ts index a59e839a7d..e8d318e5c4 100644 --- a/packages/core/e2e/local-build.test.ts +++ b/packages/core/e2e/local-build.test.ts @@ -3,7 +3,10 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { describe, expect, test } from 'vitest'; import { usesVercelWorld } from '../../utils/src/world-target'; -import { getWorkbenchAppPath } from './utils'; +import { + getWorkbenchAppPath, + isNextLazyDiscoveryEnabledForTest, +} from './utils'; interface CommandResult { stdout: string; @@ -119,7 +122,10 @@ describe.each([ expect(result.output).not.toContain('Error:'); - if (DEFERRED_BUILD_MODE_PROJECTS.has(project)) { + if ( + DEFERRED_BUILD_MODE_PROJECTS.has(project) && + isNextLazyDiscoveryEnabledForTest() + ) { const deferredBuildSupported = !result.output.includes( DEFERRED_BUILD_UNSUPPORTED_WARNING ); diff --git a/packages/core/e2e/utils.ts b/packages/core/e2e/utils.ts index 01ed6df1b3..9c74f60e9d 100644 --- a/packages/core/e2e/utils.ts +++ b/packages/core/e2e/utils.ts @@ -19,6 +19,25 @@ function splitArgs(raw: string): string[] { return value.split(/\s+/); } +function parseBooleanEnvFlag( + rawValue: string | undefined +): boolean | undefined { + const normalizedValue = rawValue?.trim().toLowerCase(); + if (!normalizedValue) { + return undefined; + } + + if (normalizedValue === '0' || normalizedValue === 'false') { + return false; + } + + return true; +} + +export function isNextLazyDiscoveryEnabledForTest(): boolean { + return parseBooleanEnvFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY) ?? true; +} + export function getWorkbenchAppPath(overrideAppName?: string): string { const explicitWorkbenchPath = process.env.WORKBENCH_APP_PATH; const appName = process.env.APP_NAME ?? overrideAppName; diff --git a/packages/next/src/builder.test.ts b/packages/next/src/builder.test.ts new file mode 100644 index 0000000000..71ab85ed53 --- /dev/null +++ b/packages/next/src/builder.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { shouldUseDeferredBuilder } from './builder.js'; + +const originalLazyDiscoveryEnv = process.env.WORKFLOW_NEXT_LAZY_DISCOVERY; + +afterEach(() => { + if (originalLazyDiscoveryEnv === undefined) { + delete process.env.WORKFLOW_NEXT_LAZY_DISCOVERY; + } else { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = originalLazyDiscoveryEnv; + } +}); + +describe('shouldUseDeferredBuilder', () => { + it('treats WORKFLOW_NEXT_LAZY_DISCOVERY=0 as disabled', () => { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '0'; + + expect(shouldUseDeferredBuilder('16.2.1')).toBe(false); + }); + + it('treats WORKFLOW_NEXT_LAZY_DISCOVERY=false as disabled', () => { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = 'false'; + + expect(shouldUseDeferredBuilder('16.2.1')).toBe(false); + }); + + it('enables deferred mode for compatible versions when the env is enabled', () => { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '1'; + + expect(shouldUseDeferredBuilder('16.2.1')).toBe(true); + }); +}); diff --git a/packages/next/src/builder.ts b/packages/next/src/builder.ts index fc9da13b3d..69ccdee374 100644 --- a/packages/next/src/builder.ts +++ b/packages/next/src/builder.ts @@ -12,8 +12,24 @@ export const WORKFLOW_DEFERRED_ENTRIES = [ let warnedAboutFlagAndVersion = false; +export function parseEnvironmentFlag( + rawValue: string | undefined +): boolean | undefined { + const normalizedValue = rawValue?.trim().toLowerCase(); + if (!normalizedValue) { + return undefined; + } + + if (normalizedValue === '0' || normalizedValue === 'false') { + return false; + } + + return true; +} + export function shouldUseDeferredBuilder(nextVersion: string): boolean { - const flagEnabled = Boolean(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY); + const flagEnabled = + parseEnvironmentFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY) ?? false; const versionCompatible = semver.gte( nextVersion, DEFERRED_BUILDER_MIN_VERSION diff --git a/packages/next/src/index.test.ts b/packages/next/src/index.test.ts index 08d22b78df..f17c5893f3 100644 --- a/packages/next/src/index.test.ts +++ b/packages/next/src/index.test.ts @@ -31,6 +31,18 @@ const { vi.mock('./builder.js', () => ({ getNextBuilder: getNextBuilderMock, + parseEnvironmentFlag: (rawValue: string | undefined) => { + const normalizedValue = rawValue?.trim().toLowerCase(); + if (!normalizedValue) { + return undefined; + } + + if (normalizedValue === '0' || normalizedValue === 'false') { + return false; + } + + return true; + }, shouldUseDeferredBuilder: shouldUseDeferredBuilderMock, WORKFLOW_DEFERRED_ENTRIES: [ '/.well-known/workflow/v1/flow', @@ -109,4 +121,34 @@ describe('withWorkflow outputFileTracingRoot', () => { workingDir: process.cwd(), }); }); + + it('preserves an explicit lazyDiscovery disable override', () => { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '0'; + + withWorkflow( + {}, + { + workflows: { + lazyDiscovery: true, + }, + } + ); + + expect(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY).toBe('0'); + }); + + it('treats an empty lazyDiscovery env override as unset', () => { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = ''; + + withWorkflow( + {}, + { + workflows: { + lazyDiscovery: true, + }, + } + ); + + expect(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY).toBe('1'); + }); }); diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index f16c45967e..e12b393c68 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -2,6 +2,7 @@ import type { NextConfig } from 'next'; import semver from 'semver'; import { getNextBuilder, + parseEnvironmentFlag, shouldUseDeferredBuilder, WORKFLOW_DEFERRED_ENTRIES, } from './builder.js'; @@ -61,8 +62,17 @@ export function withWorkflow( }; } = {} ) { - if (workflows?.lazyDiscovery) { - process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '1'; + const lazyDiscoveryOverride = parseEnvironmentFlag( + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY + ); + if (lazyDiscoveryOverride === undefined) { + if (workflows?.lazyDiscovery) { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '1'; + } + } else { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = lazyDiscoveryOverride + ? '1' + : '0'; } if (!process.env.VERCEL_DEPLOYMENT_ID) { diff --git a/packages/next/src/loader.ts b/packages/next/src/loader.ts index e04a2b219e..cd34239dd7 100644 --- a/packages/next/src/loader.ts +++ b/packages/next/src/loader.ts @@ -687,11 +687,15 @@ export default function workflowLoader( // Detect workflow patterns in the source code. const patterns = await detectPatterns(sourceForTransform); - // Always notify discovery tracking, even for `false/false`, so files that - // previously had workflow/step usage are removed from the tracked sets. - // Deferred step copy files must report using their original source path so - // deferred rebuilds can react to source edits outside generated artifacts. - if (!isDeferredStepCopyFile || deferredStepSourceMetadata?.absolutePath) { + const shouldTrackDeferredDiscovery = + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY === '1'; + + // Discovery tracking is only needed for deferred builds. Eager builds + // discover inputs up front and should ignore any stale socket metadata. + if ( + shouldTrackDeferredDiscovery && + (!isDeferredStepCopyFile || deferredStepSourceMetadata?.absolutePath) + ) { const hasSerde = patterns.hasSerde; const nextPatternState: DiscoveredPatternState = { hasWorkflow: patterns.hasUseWorkflow, diff --git a/scripts/create-test-matrix.mjs b/scripts/create-test-matrix.mjs index 085c16c937..b7efa05710 100644 --- a/scripts/create-test-matrix.mjs +++ b/scripts/create-test-matrix.mjs @@ -71,81 +71,93 @@ const DEV_TEST_CONFIGS = { }, }; -const matrix = { - app: [ - { - name: 'nextjs-turbopack', - project: 'example-nextjs-workflow-turbopack', - ...DEV_TEST_CONFIGS['nextjs-turbopack'], - }, - { - name: 'nextjs-webpack', - project: 'example-nextjs-workflow-webpack', - ...DEV_TEST_CONFIGS['nextjs-webpack'], - }, - ], -}; +function createMatrixEntry(name, project, config, overrides = {}) { + const canary = overrides.canary === true; -const newItems = []; - -for (const item of matrix.app) { - newItems.push({ ...item, canary: true }); + return { + name, + project, + ...config, + runLabel: canary ? 'canary' : 'stable', + artifactSuffix: canary ? 'canary' : 'stable', + ...overrides, + }; } -matrix.app.push(...newItems); - -// Manually add nitro -matrix.app.push({ - name: 'nitro', - project: 'workbench-nitro-workflow', - ...DEV_TEST_CONFIGS.nitro, -}); - -matrix.app.push({ - name: 'sveltekit', - project: 'workbench-sveltekit-workflow', - ...DEV_TEST_CONFIGS.sveltekit, -}); - -matrix.app.push({ - name: 'nuxt', - project: 'workbench-nuxt-workflow', - ...DEV_TEST_CONFIGS.nuxt, -}); - -matrix.app.push({ - name: 'hono', - project: 'workbench-hono-workflow', - ...DEV_TEST_CONFIGS.hono, -}); -matrix.app.push({ - name: 'vite', - project: 'workbench-vite-workflow', - ...DEV_TEST_CONFIGS.vite, -}); - -matrix.app.push({ - name: 'express', - project: 'workbench-express-workflow', - ...DEV_TEST_CONFIGS.express, -}); - -matrix.app.push({ - name: 'fastify', - project: 'workbench-fastify-workflow', - ...DEV_TEST_CONFIGS.fastify, -}); +const matrix = { + app: [], +}; -matrix.app.push({ - name: 'nest', - project: 'workbench-nest-workflow', - ...DEV_TEST_CONFIGS.nest, -}); +for (const app of [ + { + name: 'nextjs-turbopack', + project: 'example-nextjs-workflow-turbopack', + }, + { + name: 'nextjs-webpack', + project: 'example-nextjs-workflow-webpack', + }, +]) { + matrix.app.push( + createMatrixEntry(app.name, app.project, DEV_TEST_CONFIGS[app.name], { + lazyDiscovery: true, + runLabel: 'stable lazyDiscovery enabled', + artifactSuffix: 'stable-lazy-discovery-enabled', + }) + ); + matrix.app.push( + createMatrixEntry(app.name, app.project, DEV_TEST_CONFIGS[app.name], { + lazyDiscovery: false, + runLabel: 'stable lazyDiscovery disabled', + artifactSuffix: 'stable-lazy-discovery-disabled', + }) + ); + matrix.app.push( + createMatrixEntry(app.name, app.project, DEV_TEST_CONFIGS[app.name], { + canary: true, + lazyDiscovery: true, + }) + ); +} -matrix.app.push({ - name: 'astro', - project: 'workbench-astro-workflow', - ...DEV_TEST_CONFIGS.astro, -}); +matrix.app.push( + createMatrixEntry('nitro', 'workbench-nitro-workflow', DEV_TEST_CONFIGS.nitro) +); +matrix.app.push( + createMatrixEntry( + 'sveltekit', + 'workbench-sveltekit-workflow', + DEV_TEST_CONFIGS.sveltekit + ) +); +matrix.app.push( + createMatrixEntry('nuxt', 'workbench-nuxt-workflow', DEV_TEST_CONFIGS.nuxt) +); +matrix.app.push( + createMatrixEntry('hono', 'workbench-hono-workflow', DEV_TEST_CONFIGS.hono) +); +matrix.app.push( + createMatrixEntry('vite', 'workbench-vite-workflow', DEV_TEST_CONFIGS.vite) +); +matrix.app.push( + createMatrixEntry( + 'express', + 'workbench-express-workflow', + DEV_TEST_CONFIGS.express + ) +); +matrix.app.push( + createMatrixEntry( + 'fastify', + 'workbench-fastify-workflow', + DEV_TEST_CONFIGS.fastify + ) +); +matrix.app.push( + createMatrixEntry('nest', 'workbench-nest-workflow', DEV_TEST_CONFIGS.nest) +); +matrix.app.push( + createMatrixEntry('astro', 'workbench-astro-workflow', DEV_TEST_CONFIGS.astro) +); console.log(JSON.stringify(matrix)); From ec052a0a501567a50590665f3641b763858ab342 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 15 Apr 2026 12:38:05 -0700 Subject: [PATCH 2/2] Address PR review feedback --- packages/core/e2e/utils.ts | 18 ++---------------- packages/next/src/builder.test.ts | 10 ++++++++++ packages/next/src/builder.ts | 16 +--------------- packages/next/src/environment-flag.ts | 16 ++++++++++++++++ packages/next/src/index.test.ts | 12 ------------ packages/next/src/index.ts | 2 +- packages/next/src/loader.ts | 3 ++- 7 files changed, 32 insertions(+), 45 deletions(-) create mode 100644 packages/next/src/environment-flag.ts diff --git a/packages/core/e2e/utils.ts b/packages/core/e2e/utils.ts index 9c74f60e9d..bd35044e0b 100644 --- a/packages/core/e2e/utils.ts +++ b/packages/core/e2e/utils.ts @@ -5,6 +5,7 @@ import { setTimeout as sleep } from 'node:timers/promises'; import { fileURLToPath } from 'node:url'; import { createVercelWorld } from '@workflow/world-vercel'; import { onTestFailed } from 'vitest'; +import { parseEnvironmentFlag } from '../../next/src/environment-flag.js'; import type { Run } from '../src/runtime'; import { getWorld, setWorld } from '../src/runtime'; @@ -19,23 +20,8 @@ function splitArgs(raw: string): string[] { return value.split(/\s+/); } -function parseBooleanEnvFlag( - rawValue: string | undefined -): boolean | undefined { - const normalizedValue = rawValue?.trim().toLowerCase(); - if (!normalizedValue) { - return undefined; - } - - if (normalizedValue === '0' || normalizedValue === 'false') { - return false; - } - - return true; -} - export function isNextLazyDiscoveryEnabledForTest(): boolean { - return parseBooleanEnvFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY) ?? true; + return parseEnvironmentFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY) ?? true; } export function getWorkbenchAppPath(overrideAppName?: string): string { diff --git a/packages/next/src/builder.test.ts b/packages/next/src/builder.test.ts index 71ab85ed53..6e2d92035e 100644 --- a/packages/next/src/builder.test.ts +++ b/packages/next/src/builder.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest'; import { shouldUseDeferredBuilder } from './builder.js'; +import { parseEnvironmentFlag } from './environment-flag.js'; const originalLazyDiscoveryEnv = process.env.WORKFLOW_NEXT_LAZY_DISCOVERY; @@ -24,6 +25,15 @@ describe('shouldUseDeferredBuilder', () => { expect(shouldUseDeferredBuilder('16.2.1')).toBe(false); }); + it('treats WORKFLOW_NEXT_LAZY_DISCOVERY=off as disabled', () => { + process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = 'off'; + + expect(parseEnvironmentFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY)).toBe( + false + ); + expect(shouldUseDeferredBuilder('16.2.1')).toBe(false); + }); + it('enables deferred mode for compatible versions when the env is enabled', () => { process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '1'; diff --git a/packages/next/src/builder.ts b/packages/next/src/builder.ts index 69ccdee374..1f65de1858 100644 --- a/packages/next/src/builder.ts +++ b/packages/next/src/builder.ts @@ -1,6 +1,7 @@ import semver from 'semver'; import { getNextBuilderDeferred } from './builder-deferred.js'; import { getNextBuilderEager } from './builder-eager.js'; +import { parseEnvironmentFlag } from './environment-flag.js'; export const DEFERRED_BUILDER_MIN_VERSION = '16.2.0-canary.48'; @@ -12,21 +13,6 @@ export const WORKFLOW_DEFERRED_ENTRIES = [ let warnedAboutFlagAndVersion = false; -export function parseEnvironmentFlag( - rawValue: string | undefined -): boolean | undefined { - const normalizedValue = rawValue?.trim().toLowerCase(); - if (!normalizedValue) { - return undefined; - } - - if (normalizedValue === '0' || normalizedValue === 'false') { - return false; - } - - return true; -} - export function shouldUseDeferredBuilder(nextVersion: string): boolean { const flagEnabled = parseEnvironmentFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY) ?? false; diff --git a/packages/next/src/environment-flag.ts b/packages/next/src/environment-flag.ts new file mode 100644 index 0000000000..3436a7303c --- /dev/null +++ b/packages/next/src/environment-flag.ts @@ -0,0 +1,16 @@ +const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +export function parseEnvironmentFlag( + rawValue: string | undefined +): boolean | undefined { + const normalizedValue = rawValue?.trim().toLowerCase(); + if (!normalizedValue) { + return undefined; + } + + if (FALSE_ENV_VALUES.has(normalizedValue)) { + return false; + } + + return true; +} diff --git a/packages/next/src/index.test.ts b/packages/next/src/index.test.ts index f17c5893f3..fcfee06847 100644 --- a/packages/next/src/index.test.ts +++ b/packages/next/src/index.test.ts @@ -31,18 +31,6 @@ const { vi.mock('./builder.js', () => ({ getNextBuilder: getNextBuilderMock, - parseEnvironmentFlag: (rawValue: string | undefined) => { - const normalizedValue = rawValue?.trim().toLowerCase(); - if (!normalizedValue) { - return undefined; - } - - if (normalizedValue === '0' || normalizedValue === 'false') { - return false; - } - - return true; - }, shouldUseDeferredBuilder: shouldUseDeferredBuilderMock, WORKFLOW_DEFERRED_ENTRIES: [ '/.well-known/workflow/v1/flow', diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index e12b393c68..2eb00827ba 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1,8 +1,8 @@ import type { NextConfig } from 'next'; import semver from 'semver'; +import { parseEnvironmentFlag } from './environment-flag.js'; import { getNextBuilder, - parseEnvironmentFlag, shouldUseDeferredBuilder, WORKFLOW_DEFERRED_ENTRIES, } from './builder.js'; diff --git a/packages/next/src/loader.ts b/packages/next/src/loader.ts index cd34239dd7..f806c0af62 100644 --- a/packages/next/src/loader.ts +++ b/packages/next/src/loader.ts @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises'; import { connect, type Socket } from 'node:net'; import { dirname, join, relative } from 'node:path'; import { transform } from '@swc/core'; +import { parseEnvironmentFlag } from './environment-flag.js'; import { parseMessage, type SocketMessage, @@ -688,7 +689,7 @@ export default function workflowLoader( // Detect workflow patterns in the source code. const patterns = await detectPatterns(sourceForTransform); const shouldTrackDeferredDiscovery = - process.env.WORKFLOW_NEXT_LAZY_DISCOVERY === '1'; + parseEnvironmentFlag(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY) ?? false; // Discovery tracking is only needed for deferred builds. Eager builds // discover inputs up front and should ignore any stale socket metadata.