Skip to content

Commit 540a2ef

Browse files
[errors] Replace chalk import in @workfow/errors with inline ANSI shim (vercel#1915)
1 parent c56c4f6 commit 540a2ef

7 files changed

Lines changed: 101 additions & 35 deletions

File tree

.changeset/errors-no-chalk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/errors": patch
3+
---
4+
5+
Replace the `chalk` import in `@workflow/errors/ansi` with a tiny inline ANSI shim. `@workflow/errors/ansi` is reachable from the workflow-VM bundle (via `@workflow/core/workflow``context-errors``context-violation-error` → here), and `chalk` pulls in `supports-color`, which calls `require('os')` at module load — crashing every workflow with `ReferenceError: require is not defined` in the sandboxed VM.

packages/errors/__mocks__/chalk.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

packages/errors/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
},
4343
"dependencies": {
4444
"@workflow/utils": "workspace:*",
45-
"chalk": "5.6.2",
4645
"ms": "2.1.3"
4746
}
4847
}

packages/errors/src/ansi.test.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,30 @@
11
import { describe, expect, it, vi } from 'vitest';
2-
import * as Ansi from './ansi.js';
32

4-
// Render ANSI styles as HTML-like tags in snapshots so they're readable.
5-
vi.mock('chalk');
3+
// Render styles as readable HTML-like tags in snapshots so a reviewer can
4+
// see at a glance which fragments are colored and how. The production
5+
// implementation in `./internal-chalk.ts` emits ANSI SGR escapes and
6+
// short-circuits to identity functions when there's no TTY (and in the
7+
// workflow VM, where `globalThis.process` is undefined).
8+
vi.mock('./internal-chalk.js', () => {
9+
const tag =
10+
(name: string) =>
11+
(s: string): string =>
12+
`<${name}>${s}</${name}>`;
13+
return {
14+
default: {
15+
bold: tag('b'),
16+
dim: tag('dim'),
17+
italic: tag('i'),
18+
red: tag('red'),
19+
blue: tag('blue'),
20+
cyan: tag('cyan'),
21+
yellow: tag('yellow'),
22+
magenta: tag('magenta'),
23+
},
24+
};
25+
});
26+
27+
const Ansi = await import('./ansi.js');
628

729
describe('Ansi.frame', () => {
830
it('renders a single-line title with no contents', () => {
@@ -62,7 +84,10 @@ describe('Ansi.hint / note / help / docs', () => {
6284
expect(
6385
Ansi.note(['read more:', 'https://example.com'])
6486
).toMatchInlineSnapshot(
65-
`"<blue><b>note:</b> read more:\nhttps://example.com</blue>"`
87+
`
88+
"<blue><b>note:</b> read more:
89+
https://example.com</blue>"
90+
`
6691
);
6792
});
6893

packages/errors/src/ansi.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import chalk from 'chalk';
1+
// Imported from a sibling module rather than `chalk` proper so this file
2+
// (and everything that statically imports it — including the workflow-VM
3+
// reachable `context-violation-error.ts`) doesn't pull in chalk's
4+
// `supports-color` / `require('os')` chain. See `./internal-chalk.ts`
5+
// for the full rationale and the test mock that swaps it out.
6+
import chalk from './internal-chalk.js';
27

38
/**
49
* Helpers for composing structured, human-friendly error messages.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Tiny inline `chalk` replacement.
3+
*
4+
* `@workflow/errors/ansi` is reachable from the workflow-VM bundle (via
5+
* `@workflow/core/workflow` → `context-errors` → `context-violation-error`
6+
* → here), and the workflow VM has no `require()`. The real `chalk` package
7+
* pulls in `supports-color`, which calls `require('os')` at module load —
8+
* so importing `chalk` here crashes every workflow with
9+
* `ReferenceError: require is not defined`.
10+
*
11+
* This module exposes the subset of chalk's call surface that `ansi.ts`
12+
* uses (`bold`, `dim`, `italic`, `red`, `blue`, `cyan`, `yellow`,
13+
* `magenta`). Color detection mirrors chalk's defaults at a coarse level:
14+
* `FORCE_COLOR` forces on, `NO_COLOR` forces off, otherwise we emit ANSI
15+
* only on a TTY stdout. In the workflow VM `process` is absent so this
16+
* evaluates to "no color" and the helpers become identity functions —
17+
* which is what the runtime wants anyway, since the host catches and
18+
* re-renders the error.
19+
*
20+
* Exported from its own module so tests can replace it with `vi.mock` to
21+
* render styles as readable HTML-like tags in snapshots without dragging
22+
* `chalk` (and its transitive `supports-color` / `require('os')` chain)
23+
* back into the workflow VM bundle.
24+
*/
25+
26+
const colorEnabled = (() => {
27+
const p = (globalThis as { process?: NodeJS.Process }).process;
28+
if (!p?.env) return false;
29+
if (p.env.FORCE_COLOR && p.env.FORCE_COLOR !== '0') return true;
30+
if (p.env.NO_COLOR) return false;
31+
return Boolean(p.stdout?.isTTY);
32+
})();
33+
34+
const sgr =
35+
(open: number, close: number) =>
36+
(s: string): string =>
37+
colorEnabled ? `\x1b[${open}m${s}\x1b[${close}m` : s;
38+
39+
export interface InternalChalk {
40+
bold: (s: string) => string;
41+
dim: (s: string) => string;
42+
italic: (s: string) => string;
43+
red: (s: string) => string;
44+
blue: (s: string) => string;
45+
cyan: (s: string) => string;
46+
yellow: (s: string) => string;
47+
magenta: (s: string) => string;
48+
}
49+
50+
const chalk: InternalChalk = {
51+
bold: sgr(1, 22),
52+
dim: sgr(2, 22),
53+
italic: sgr(3, 23),
54+
red: sgr(31, 39),
55+
blue: sgr(34, 39),
56+
cyan: sgr(36, 39),
57+
yellow: sgr(33, 39),
58+
magenta: sgr(35, 39),
59+
};
60+
61+
export default chalk;

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)