diff --git a/src/api/schema.zig b/src/api/schema.zig index ccfa99d386..f4f70201b9 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -321,8 +321,9 @@ pub const ByteWriter = Writer(*std.io.FixedBufferStream([]u8)); pub const FileWriter = Writer(std.fs.File); pub const api = struct { + // these are in sync with BunLoaderType in headers-handwritten.h pub const Loader = enum(u8) { - _none = 255, + _none = 254, jsx = 1, js = 2, ts = 3, diff --git a/src/bun.js/ModuleLoader.zig b/src/bun.js/ModuleLoader.zig index 8398f08614..09921adc0a 100644 --- a/src/bun.js/ModuleLoader.zig +++ b/src/bun.js/ModuleLoader.zig @@ -1600,9 +1600,10 @@ pub export fn Bun__transpileFile( ret: *jsc.ErrorableResolvedSource, allow_promise: bool, is_commonjs_require: bool, - force_loader_type: bun.options.Loader.Optional, + _force_loader_type: bun.schema.api.Loader, ) ?*anyopaque { jsc.markBinding(@src()); + const force_loader_type: bun.options.Loader.Optional = .fromAPI(_force_loader_type); var log = logger.Log.init(jsc_vm.transpiler.allocator); defer log.deinit(); diff --git a/src/bun.js/bindings/JSCommonJSExtensions.cpp b/src/bun.js/bindings/JSCommonJSExtensions.cpp index 4999c8bb8a..1f0fcaf36a 100644 --- a/src/bun.js/bindings/JSCommonJSExtensions.cpp +++ b/src/bun.js/bindings/JSCommonJSExtensions.cpp @@ -138,32 +138,38 @@ void JSCommonJSExtensions::finishCreation(JSC::VM& vm) extern "C" void NodeModuleModule__onRequireExtensionModify( Zig::GlobalObject* globalObject, const BunString* key, - uint32_t kind, + BunLoaderType loader, JSC::JSValue value); +extern "C" void NodeModuleModule__onRequireExtensionModifyNonFunction( + Zig::GlobalObject* globalObject, + const BunString* key); + void onAssign(Zig::GlobalObject* globalObject, JSC::PropertyName propertyName, JSC::JSValue value) { if (propertyName.isSymbol()) return; auto* name = propertyName.publicName(); if (!name->startsWith('.')) return; BunString ext = Bun::toString(name); - uint32_t kind = 0; JSC::CallData callData = JSC::getCallData(value); + if (callData.type == JSC::CallData::Type::None) { + return NodeModuleModule__onRequireExtensionModifyNonFunction(globalObject, &ext); + } + + BunLoaderType loader = BunLoaderTypeNone; if (callData.type == JSC::CallData::Type::Native) { auto* untaggedPtr = callData.native.function.untaggedPtr(); if (untaggedPtr == &jsLoaderJS) { - kind = 1; + loader = BunLoaderTypeJS; } else if (untaggedPtr == &jsLoaderJSON) { - kind = 2; + loader = BunLoaderTypeJSON; } else if (untaggedPtr == &jsLoaderNode) { - kind = 3; + loader = BunLoaderTypeNAPI; } else if (untaggedPtr == &jsLoaderTS) { - kind = 4; + loader = BunLoaderTypeTS; } - } else if (callData.type == JSC::CallData::Type::None) { - kind = -1; } - NodeModuleModule__onRequireExtensionModify(globalObject, &ext, kind, value); + NodeModuleModule__onRequireExtensionModify(globalObject, &ext, loader, value); } bool JSCommonJSExtensions::defineOwnProperty(JSC::JSObject* object, JSC::JSGlobalObject* globalObject, JSC::PropertyName propertyName, const JSC::PropertyDescriptor& descriptor, bool shouldThrow) diff --git a/src/bun.js/bindings/NodeModuleModule.zig b/src/bun.js/bindings/NodeModuleModule.zig index 22eef36724..56545462b8 100644 --- a/src/bun.js/bindings/NodeModuleModule.zig +++ b/src/bun.js/bindings/NodeModuleModule.zig @@ -86,49 +86,46 @@ extern fn JSCommonJSExtensions__swapRemove(global: *jsc.JSGlobalObject, index: u // Memory management is complicated because JSValues are stored in gc-visitable // WriteBarriers in C++ but the hash map for extensions is in Zig for flexibility. -fn onRequireExtensionModify(global: *jsc.JSGlobalObject, str: []const u8, kind: i32, value: jsc.JSValue) !void { - bun.assert(kind >= -1 and kind <= 4); +fn onRequireExtensionModify(global: *jsc.JSGlobalObject, str: []const u8, loader: bun.schema.api.Loader, value: jsc.JSValue) bun.OOM!void { const vm = global.bunVM(); const list = &vm.commonjs_custom_extensions; defer vm.transpiler.resolver.opts.extra_cjs_extensions = list.keys(); const is_built_in = bun.options.defaultLoaders.get(str) != null; - if (kind >= 0) { - const loader: CustomLoader = switch (kind) { - 1 => .{ .loader = .js }, - 2 => .{ .loader = .json }, - 3 => .{ .loader = .napi }, - 4 => .{ .loader = .ts }, - else => .{ .custom = undefined }, // to be filled in later - }; - const gop = try list.getOrPut(bun.default_allocator, str); - if (!gop.found_existing) { - const dupe = try bun.default_allocator.dupe(u8, str); - gop.key_ptr.* = dupe; - if (is_built_in) { - vm.has_mutated_built_in_extensions += 1; + + const gop = try list.getOrPut(bun.default_allocator, str); + if (!gop.found_existing) { + gop.key_ptr.* = try bun.default_allocator.dupe(u8, str); + if (is_built_in) { + vm.has_mutated_built_in_extensions += 1; + } + + gop.value_ptr.* = if (loader != ._none) + .{ .loader = .fromAPI(loader) } + else + .{ .custom = .create(value, global) }; + } else { + if (loader != ._none) { + switch (gop.value_ptr.*) { + .loader => {}, + .custom => |*strong| strong.deinit(), } - gop.value_ptr.* = switch (loader) { - .loader => loader, - .custom => .{ - .custom = .create(value, global), - }, - }; + gop.value_ptr.* = .{ .loader = .fromAPI(loader) }; } else { - switch (loader) { - .loader => { - switch (gop.value_ptr.*) { - .loader => {}, - .custom => |*strong| strong.deinit(), - } - gop.value_ptr.* = loader; - }, - .custom => switch (gop.value_ptr.*) { - .loader => gop.value_ptr.* = .{ .custom = .create(value, global) }, - .custom => |*strong| strong.set(global, value), - }, + switch (gop.value_ptr.*) { + .loader => gop.value_ptr.* = .{ .custom = .create(value, global) }, + .custom => |*strong| strong.set(global, value), } } - } else if (list.fetchSwapRemove(str)) |prev| { + } +} + +fn onRequireExtensionModifyNonFunction(global: *JSGlobalObject, str: []const u8) bun.OOM!void { + const vm = global.bunVM(); + const list = &vm.commonjs_custom_extensions; + defer vm.transpiler.resolver.opts.extra_cjs_extensions = list.keys(); + const is_built_in = bun.options.defaultLoaders.get(str) != null; + + if (list.fetchSwapRemove(str)) |prev| { bun.default_allocator.free(prev.key); if (is_built_in) { vm.has_mutated_built_in_extensions -= 1; @@ -160,20 +157,34 @@ pub fn findLongestRegisteredExtension(vm: *jsc.VirtualMachine, filename: []const fn onRequireExtensionModifyBinding( global: *jsc.JSGlobalObject, str: *const bun.String, - kind: i32, + loader: bun.schema.api.Loader, value: jsc.JSValue, ) callconv(.c) void { var sfa_state = std.heap.stackFallback(8192, bun.default_allocator); const alloc = sfa_state.get(); const str_slice = str.toUTF8(alloc); defer str_slice.deinit(); - onRequireExtensionModify(global, str_slice.slice(), kind, value) catch |err| switch (err) { + onRequireExtensionModify(global, str_slice.slice(), loader, value) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }; +} + +fn onRequireExtensionModifyNonFunctionBinding( + global: *jsc.JSGlobalObject, + str: *const bun.String, +) callconv(.c) void { + var sfa_state = std.heap.stackFallback(8192, bun.default_allocator); + const alloc = sfa_state.get(); + const str_slice = str.toUTF8(alloc); + defer str_slice.deinit(); + onRequireExtensionModifyNonFunction(global, str_slice.slice()) catch |err| switch (err) { error.OutOfMemory => bun.outOfMemory(), }; } comptime { @export(&onRequireExtensionModifyBinding, .{ .name = "NodeModuleModule__onRequireExtensionModify" }); + @export(&onRequireExtensionModifyNonFunctionBinding, .{ .name = "NodeModuleModule__onRequireExtensionModifyNonFunction" }); } const bun = @import("bun"); diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 26d03cbb10..956085e43e 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -236,10 +236,9 @@ const JSErrorCode JSErrorCodeOutOfMemoryError = 8; const JSErrorCode JSErrorCodeStackOverflow = 253; const JSErrorCode JSErrorCodeUserErrorCode = 254; -// Must be kept in sync. +// Must be kept in sync with bun.schema.api.Loader in schema.zig typedef uint8_t BunLoaderType; const BunLoaderType BunLoaderTypeNone = 254; -// Must match api/schema.zig Loader enum values const BunLoaderType BunLoaderTypeJSX = 1; const BunLoaderType BunLoaderTypeJS = 2; const BunLoaderType BunLoaderTypeTS = 3; diff --git a/src/options.zig b/src/options.zig index 553ace7668..6a32f61641 100644 --- a/src/options.zig +++ b/src/options.zig @@ -641,6 +641,14 @@ pub const Loader = enum(u8) { pub fn unwrap(opt: Optional) ?Loader { return if (opt == .none) null else @enumFromInt(@intFromEnum(opt)); } + + pub fn fromAPI(loader: bun.schema.api.Loader) Optional { + if (loader == ._none) { + return .none; + } + const l: Loader = .fromAPI(loader); + return @enumFromInt(@intFromEnum(l)); + } }; pub fn isCSS(this: Loader) bool { diff --git a/test/regression/issue/require-extensions-override.test.ts b/test/regression/issue/require-extensions-override.test.ts new file mode 100644 index 0000000000..6b7080ce17 --- /dev/null +++ b/test/regression/issue/require-extensions-override.test.ts @@ -0,0 +1,456 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("Module._extensions override should not allow TypeScript syntax in .js files", async () => { + using dir = tempDir("module-extensions-ts-in-js", { + "index.js": ` + const Module = require("module"); + const orig = Module._extensions[".js"]; + + // Override the .js extension handler with a wrapper + Module._extensions[".js"] = (m, f) => { + return orig(m, f); + }; + + // This should error because it has TypeScript syntax in a .js file + try { + require("./typescript-syntax.js"); + console.log("ERROR: Should have failed to parse TypeScript syntax in .js file"); + process.exit(1); + } catch (err) { + console.log("SUCCESS: Correctly rejected TypeScript syntax in .js file"); + process.exit(0); + } + `, + "typescript-syntax.js": ` + const value: string = "hello"; + export { value }; + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(exitCode).toBe(0); + expect(stdout).toContain("SUCCESS: Correctly rejected TypeScript syntax in .js file"); +}); + +test("Module._extensions override should not allow JSX syntax in .js files", async () => { + using dir = tempDir("module-extensions-jsx-in-js", { + "index.js": ` + const Module = require("module"); + const orig = Module._extensions[".js"]; + + // Override the .js extension handler + Module._extensions[".js"] = (m, f) => { + console.log("Loading:", f); + return orig(m, f); + }; + + // This should error because it has JSX syntax in a .js file + try { + require("./jsx-syntax.js"); + console.log("ERROR: Should have failed to parse JSX syntax in .js file"); + process.exit(1); + } catch (err) { + console.log("SUCCESS: Correctly rejected JSX syntax in .js file"); + process.exit(0); + } + `, + "jsx-syntax.js": ` + const element =