Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/large-inline-sourcemap-remap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/core': patch
---

Harden workflow error stack remapping for large inline sourcemaps.
36 changes: 36 additions & 0 deletions packages/core/src/source-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { remapErrorStack } from './source-map.js';

describe('remapErrorStack', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('extracts inline sourcemaps without regex matching the full bundle', () => {
const sourceMap = Buffer.from(
JSON.stringify({
version: 3,
sources: ['workflows/example.ts'],
names: [],
mappings: '',
})
).toString('base64');
const workflowCode = [
`const padding = "${'a'.repeat(1024 * 1024)}";`,
`//# sourceMappingURL=data:application/json;base64,${sourceMap}`,
].join('\n');
const match = vi.spyOn(String.prototype, 'match');

remapErrorStack(
'Error: boom\n at example (workflow.js:1:1)',
'workflow.js',
workflowCode
);

expect(
match.mock.calls.some(([pattern]) =>
String(pattern).includes('sourceMappingURL')
)
).toBe(false);
});
});
61 changes: 54 additions & 7 deletions packages/core/src/source-map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping';

function isBase64Char(code: number): boolean {
return (
(code >= 0x41 && code <= 0x5a) ||
(code >= 0x61 && code <= 0x7a) ||
(code >= 0x30 && code <= 0x39) ||
code === 0x2b ||
code === 0x2f ||
code === 0x3d
);
}

function extractInlineSourceMapBase64(source: string): string | undefined {
const sourceMapPrefix = '//# sourceMappingURL=data:application/json';
const base64Marker = ';base64,';
let scanFrom = 0;

while (scanFrom < source.length) {
const prefixIdx = source.indexOf(sourceMapPrefix, scanFrom);
if (prefixIdx === -1) return undefined;

const base64Start = source.indexOf(
base64Marker,
prefixIdx + sourceMapPrefix.length
);
if (base64Start === -1) {
scanFrom = prefixIdx + sourceMapPrefix.length;
continue;
}

const commaBefore = source.indexOf(',', prefixIdx);
if (commaBefore !== -1 && commaBefore < base64Start) {
scanFrom = base64Start + base64Marker.length;
continue;
}

const valueStart = base64Start + base64Marker.length;
let valueEnd = valueStart;
while (valueEnd < source.length) {
if (!isBase64Char(source.charCodeAt(valueEnd))) {
break;
}
valueEnd++;
}

if (valueEnd > valueStart) {
return source.slice(valueStart, valueEnd);
}
scanFrom = valueEnd;
}
}

/**
* Remaps an error stack trace using inline source maps to show original source locations.
*
Expand All @@ -13,16 +64,12 @@ export function remapErrorStack(
filename: string,
workflowCode: string
): string {
// Extract inline source map from workflow code
const sourceMapMatch = workflowCode.match(
/\/\/# sourceMappingURL=data:application\/json;base64,(.+)/
);
if (!sourceMapMatch) {
const base64 = extractInlineSourceMapBase64(workflowCode);
if (!base64) {
return stack; // No source map found
}

try {
const base64 = sourceMapMatch[1];
const sourceMapJson = Buffer.from(base64, 'base64').toString('utf-8');
const sourceMapData = JSON.parse(sourceMapJson);

Expand Down Expand Up @@ -66,7 +113,7 @@ export function remapErrorStack(
});

return remappedLines.join('\n');
} catch (e) {
} catch {
// If source map processing fails, return original stack
return stack;
}
Expand Down
Loading