Files
vercel__workflow/packages/builders/src/resolve-sourcemap.test.ts
Pranay Prakash 37312edd0a Default source maps to dev-on / prod-off (#2529)
* Default source maps to dev-on / prod-off

Inline source maps are embedded in the step bundle and the intermediate
workflow VM bundle, which bloats production function bundles (a problem for
the Vercel 250MB limit) even though maps only help when reading a stack trace.

Make the default environment-aware in @workflow/builders: inline in
development (next dev / nitro dev / Vite-based dev servers, detected via
config.watch or NODE_ENV=development) and off in production. The `sourcemap`
config option and `WORKFLOW_SOURCEMAP` env var still override in either
environment. A production build with no override also drops the
source-map-support shim from the Vercel step function.

Keep runtime stack remapping graceful and fast when maps are absent
(@workflow/core): short-circuit when no frame references the workflow file
and memoize the parsed map (or its absence) per bundle, so production failures
don't rescan the bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(e2e): make source-map expectations match dev-on/prod-off default

The e2e error-stack tests gate source-map assertions on hasWorkflowSourceMaps()
and hasStepSourceMaps(). Now that source maps default to off in production
builds, update those helpers:

- hasWorkflowSourceMaps(): false for all production builds (local prod,
  postgres, Vercel — keyed off DEV_TEST_CONFIG), and exclude nest in dev (the
  Nest integration builds with watch:false / no NODE_ENV=development, so its
  bundles have no maps).
- hasStepSourceMaps(): nest now resolves to a production build (maps off) in
  both dev and prod.

Add unit cases for the dev-vs-prod and nest behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:28:26 -07:00

231 lines
7.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { BaseBuilder } from './base-builder.js';
import type { SourcemapMode, StandaloneConfig } from './types.js';
/**
* Minimal subclass that exposes the protected `resolveSourcemap()` and
* `sourcemapsEnabled` members for testing.
*/
class TestBuilder extends BaseBuilder {
async build(): Promise<void> {
// no-op
}
public callResolveSourcemap(defaultMode: SourcemapMode): SourcemapMode {
return this.resolveSourcemap(defaultMode);
}
public get publicSourcemapsEnabled(): boolean {
return this.sourcemapsEnabled;
}
public get publicDefaultSourcemapMode(): SourcemapMode {
return this.defaultSourcemapMode;
}
public get publicIsDevelopmentBuild(): boolean {
return this.isDevelopmentBuild;
}
}
function createBuilder(
sourcemap?: SourcemapMode,
watch?: boolean
): TestBuilder {
const config: StandaloneConfig = {
buildTarget: 'standalone',
workingDir: '/tmp/workflow-test',
dirs: ['.'],
stepsBundlePath: '',
workflowsBundlePath: '',
webhookBundlePath: '',
sourcemap,
watch,
};
return new TestBuilder(config);
}
describe('resolveSourcemap', () => {
const originalEnv = process.env.WORKFLOW_SOURCEMAP;
beforeEach(() => {
delete process.env.WORKFLOW_SOURCEMAP;
});
afterEach(() => {
if (originalEnv === undefined) {
delete process.env.WORKFLOW_SOURCEMAP;
} else {
process.env.WORKFLOW_SOURCEMAP = originalEnv;
}
});
it('returns the default when no config or env var is set', () => {
const builder = createBuilder();
expect(builder.callResolveSourcemap('inline')).toBe('inline');
expect(builder.callResolveSourcemap(false)).toBe(false);
expect(builder.callResolveSourcemap(true)).toBe(true);
});
it('prefers explicit config over the default', () => {
expect(createBuilder(false).callResolveSourcemap('inline')).toBe(false);
expect(createBuilder('external').callResolveSourcemap('inline')).toBe(
'external'
);
expect(createBuilder('linked').callResolveSourcemap(false)).toBe('linked');
expect(createBuilder(true).callResolveSourcemap('inline')).toBe(true);
});
it('prefers explicit config over environment variable', () => {
process.env.WORKFLOW_SOURCEMAP = 'inline';
expect(createBuilder(false).callResolveSourcemap('inline')).toBe(false);
expect(createBuilder('external').callResolveSourcemap('inline')).toBe(
'external'
);
});
it('uses environment variable when config is not set', () => {
process.env.WORKFLOW_SOURCEMAP = 'false';
expect(createBuilder().callResolveSourcemap('inline')).toBe(false);
process.env.WORKFLOW_SOURCEMAP = 'true';
expect(createBuilder().callResolveSourcemap(false)).toBe(true);
for (const mode of ['inline', 'linked', 'external', 'both'] as const) {
process.env.WORKFLOW_SOURCEMAP = mode;
expect(createBuilder().callResolveSourcemap('inline')).toBe(mode);
}
});
it('accepts "0" / "1" as environment variable aliases for false / true', () => {
process.env.WORKFLOW_SOURCEMAP = '0';
expect(createBuilder().callResolveSourcemap('inline')).toBe(false);
process.env.WORKFLOW_SOURCEMAP = '1';
expect(createBuilder().callResolveSourcemap(false)).toBe(true);
});
it('falls back to default when env var is empty or unrecognized', () => {
process.env.WORKFLOW_SOURCEMAP = '';
expect(createBuilder().callResolveSourcemap('inline')).toBe('inline');
// Suppress the expected warning
const originalWarn = console.warn;
console.warn = () => {};
try {
process.env.WORKFLOW_SOURCEMAP = 'nonsense';
expect(createBuilder().callResolveSourcemap('inline')).toBe('inline');
} finally {
console.warn = originalWarn;
}
});
});
describe('defaultSourcemapMode / isDevelopmentBuild', () => {
const originalNodeEnv = process.env.NODE_ENV;
const originalSourcemapEnv = process.env.WORKFLOW_SOURCEMAP;
beforeEach(() => {
delete process.env.WORKFLOW_SOURCEMAP;
});
afterEach(() => {
if (originalNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
if (originalSourcemapEnv === undefined) {
delete process.env.WORKFLOW_SOURCEMAP;
} else {
process.env.WORKFLOW_SOURCEMAP = originalSourcemapEnv;
}
});
it('defaults to off in production (no watch, NODE_ENV not development)', () => {
process.env.NODE_ENV = 'production';
const builder = createBuilder();
expect(builder.publicIsDevelopmentBuild).toBe(false);
expect(builder.publicDefaultSourcemapMode).toBe(false);
});
it('defaults to inline when config.watch is true', () => {
// Even with a production NODE_ENV, an active watch/dev server opts in.
process.env.NODE_ENV = 'production';
const builder = createBuilder(undefined, true);
expect(builder.publicIsDevelopmentBuild).toBe(true);
expect(builder.publicDefaultSourcemapMode).toBe('inline');
});
it('defaults to inline when NODE_ENV is development', () => {
process.env.NODE_ENV = 'development';
const builder = createBuilder();
expect(builder.publicIsDevelopmentBuild).toBe(true);
expect(builder.publicDefaultSourcemapMode).toBe('inline');
});
});
describe('sourcemapsEnabled', () => {
const originalNodeEnv = process.env.NODE_ENV;
const originalEnv = process.env.WORKFLOW_SOURCEMAP;
beforeEach(() => {
delete process.env.WORKFLOW_SOURCEMAP;
// Pin to production so the environment-aware default is deterministic;
// individual tests opt into dev via watch/NODE_ENV as needed.
process.env.NODE_ENV = 'production';
});
afterEach(() => {
if (originalNodeEnv === undefined) {
delete process.env.NODE_ENV;
} else {
process.env.NODE_ENV = originalNodeEnv;
}
if (originalEnv === undefined) {
delete process.env.WORKFLOW_SOURCEMAP;
} else {
process.env.WORKFLOW_SOURCEMAP = originalEnv;
}
});
it('is false by default in production', () => {
expect(createBuilder().publicSourcemapsEnabled).toBe(false);
});
it('is true by default in development (watch)', () => {
expect(createBuilder(undefined, true).publicSourcemapsEnabled).toBe(true);
});
it('is true by default in development (NODE_ENV)', () => {
process.env.NODE_ENV = 'development';
expect(createBuilder().publicSourcemapsEnabled).toBe(true);
});
it('is false when config sourcemap is false', () => {
expect(createBuilder(false).publicSourcemapsEnabled).toBe(false);
});
it('is true for any non-false config value', () => {
for (const mode of [
true,
'inline',
'linked',
'external',
'both',
] as const) {
expect(createBuilder(mode).publicSourcemapsEnabled).toBe(true);
}
});
it('is true in production when WORKFLOW_SOURCEMAP env opts in', () => {
process.env.WORKFLOW_SOURCEMAP = 'inline';
expect(createBuilder().publicSourcemapsEnabled).toBe(true);
});
it('is false when WORKFLOW_SOURCEMAP env is false', () => {
process.env.WORKFLOW_SOURCEMAP = 'false';
expect(createBuilder().publicSourcemapsEnabled).toBe(false);
});
});