Skip to content

Commit dd77c52

Browse files
fix(magic-string): throw TypeError for non-string content args (#8905)
## Summary - Add JS-side type validation wrappers for all MagicString methods that accept content (append, prepend, appendLeft, appendRight, prependLeft, prependRight, overwrite, update) - Throws TypeError with messages matching the JS magic-string library instead of napi-rs's generic Error - Un-skips 3 related "should throw when given non-string content" tests ## Test plan - [x] just test-node magic-string/MagicString -- all 915 tests pass, 0 failed - [x] just test-node magic-string/rolldown-magic-string -- all 33 JS tests pass - [x] Un-skipped non-string content tests for append, overwrite, and update all pass
1 parent f6cb29d commit dd77c52

File tree

3 files changed

+78
-4
lines changed

3 files changed

+78
-4
lines changed

packages/rolldown/src/binding-magic-string.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,80 @@ Object.defineProperty(NativeBindingMagicString.prototype, 'isRolldownMagicString
1111
configurable: false,
1212
});
1313

14+
// Validate content type to match JS magic-string behavior.
15+
// napi-rs throws a generic Error on type mismatch, but JS magic-string throws TypeError.
16+
function assertString(content: unknown, msg: string): asserts content is string {
17+
if (typeof content !== 'string') throw new TypeError(msg);
18+
}
19+
20+
// Save native method refs before overriding.
21+
// eslint-disable-next-line @typescript-eslint/unbound-method
22+
const nativeAppend = NativeBindingMagicString.prototype.append;
23+
// eslint-disable-next-line @typescript-eslint/unbound-method
24+
const nativePrepend = NativeBindingMagicString.prototype.prepend;
25+
// eslint-disable-next-line @typescript-eslint/unbound-method
26+
const nativeAppendLeft = NativeBindingMagicString.prototype.appendLeft;
27+
// eslint-disable-next-line @typescript-eslint/unbound-method
28+
const nativeAppendRight = NativeBindingMagicString.prototype.appendRight;
29+
// eslint-disable-next-line @typescript-eslint/unbound-method
30+
const nativePrependLeft = NativeBindingMagicString.prototype.prependLeft;
31+
// eslint-disable-next-line @typescript-eslint/unbound-method
32+
const nativePrependRight = NativeBindingMagicString.prototype.prependRight;
33+
// eslint-disable-next-line @typescript-eslint/unbound-method
34+
const nativeOverwrite = NativeBindingMagicString.prototype.overwrite;
35+
// eslint-disable-next-line @typescript-eslint/unbound-method
36+
const nativeUpdate = NativeBindingMagicString.prototype.update;
37+
38+
NativeBindingMagicString.prototype.append = function (content: any): any {
39+
assertString(content, 'outro content must be a string');
40+
return nativeAppend.call(this, content);
41+
};
42+
43+
NativeBindingMagicString.prototype.prepend = function (content: any): any {
44+
assertString(content, 'outro content must be a string');
45+
return nativePrepend.call(this, content);
46+
};
47+
48+
NativeBindingMagicString.prototype.appendLeft = function (index: any, content: any): any {
49+
assertString(content, 'inserted content must be a string');
50+
return nativeAppendLeft.call(this, index, content);
51+
};
52+
53+
NativeBindingMagicString.prototype.appendRight = function (index: any, content: any): any {
54+
assertString(content, 'inserted content must be a string');
55+
return nativeAppendRight.call(this, index, content);
56+
};
57+
58+
NativeBindingMagicString.prototype.prependLeft = function (index: any, content: any): any {
59+
assertString(content, 'inserted content must be a string');
60+
return nativePrependLeft.call(this, index, content);
61+
};
62+
63+
NativeBindingMagicString.prototype.prependRight = function (index: any, content: any): any {
64+
assertString(content, 'inserted content must be a string');
65+
return nativePrependRight.call(this, index, content);
66+
};
67+
68+
NativeBindingMagicString.prototype.overwrite = function (
69+
start: any,
70+
end: any,
71+
content: any,
72+
options?: any,
73+
): any {
74+
assertString(content, 'replacement content must be a string');
75+
return nativeOverwrite.call(this, start, end, content, options);
76+
};
77+
78+
NativeBindingMagicString.prototype.update = function (
79+
start: any,
80+
end: any,
81+
content: any,
82+
options?: any,
83+
): any {
84+
assertString(content, 'replacement content must be a string');
85+
return nativeUpdate.call(this, start, end, content, options);
86+
};
87+
1488
// Override replace/replaceAll to support RegExp patterns.
1589
// String patterns delegate to the native Rust implementation.
1690
// RegExp patterns delegate to native replaceRegex which uses the regress crate

packages/rolldown/tests/magic-string/MagicString.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('MagicString', () => {
3737
assert.strictEqual(s.append('xyz'), s);
3838
});
3939

40-
it.skip('should throw when given non-string content', () => {
40+
it('should throw when given non-string content', () => {
4141
const s = new MagicString('');
4242
assert.throws(() => s.append([]), TypeError);
4343
});
@@ -1005,7 +1005,7 @@ describe('MagicString', () => {
10051005
);
10061006
});
10071007

1008-
it.skip('should throw when given non-string content', () => {
1008+
it('should throw when given non-string content', () => {
10091009
const s = new MagicString('');
10101010
assert.throws(() => s.overwrite(0, 1, []), TypeError);
10111011
});
@@ -1146,7 +1146,7 @@ describe('MagicString', () => {
11461146
);
11471147
});
11481148

1149-
it.skip('should throw when given non-string content', () => {
1149+
it('should throw when given non-string content', () => {
11501150
const s = new MagicString('');
11511151
assert.throws(() => s.update(0, 1, []), TypeError);
11521152
});

packages/rolldown/tests/magic-string/download-tests.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const SKIP_DESCRIBE_BLOCKS = [
8282

8383
// Individual tests to skip (by partial match of test name)
8484
const SKIP_TESTS = [
85-
'should throw when given non-string content', // error handling differs
85+
// Note: 'should throw when given non-string content' now works via JS-side TypeError wrapper
8686
// Note: 'should throw' broad pattern removed — overlapping replacement error tests now pass
8787
// Remaining 'should throw' tests are covered by specific patterns (non-string content, negative indices)
8888
// options-specific skips

0 commit comments

Comments
 (0)