Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bf8f5c3
feat(mdx): add support for MDX file handling and compilation
benpsnyder Feb 15, 2026
af5930d
feat(mdx): enhance MDX support with new features and tests
benpsnyder Feb 15, 2026
8200ac8
fix(mdx): improve handling of hard and soft breaks in inline content
benpsnyder Feb 15, 2026
d50efea
refactor(mdx): streamline MDX object creation and improve option parsing
benpsnyder Feb 15, 2026
bcf787e
fix(mdx): enhance error handling and improve option parsing
benpsnyder Feb 15, 2026
d948994
fix(tests): add symlink for node_modules in MDX test cases
benpsnyder Feb 15, 2026
30a8235
fix(tests): refactor node_modules symlink handling in MDX tests
benpsnyder Feb 15, 2026
c4bab2f
fix(headers): update BunLoaderType constants for YAML, MD, and MDX
benpsnyder Feb 15, 2026
e36a785
fix(tests): correct exit code assertion in MDX tests
benpsnyder Feb 15, 2026
29316ea
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 16, 2026
373e1e5
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 17, 2026
7654783
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 17, 2026
c3dae08
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 18, 2026
5d96a59
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 18, 2026
ef31f68
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 19, 2026
6c39971
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 23, 2026
6dc8092
Enhance MDX parsing to support multiline import/export statements and…
benpsnyder Feb 23, 2026
f754839
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 24, 2026
350db9b
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 25, 2026
29ae53e
Merge branch 'main' into feat/mdx-support
benpsnyder Feb 28, 2026
6e377cc
fix(mdx): enhance argument handling to support file-like patterns
benpsnyder Feb 28, 2026
f9128d8
feat(mdx): improve hostname and port parsing in argument handling
benpsnyder Feb 28, 2026
0ea6b3a
refactor(mdx): move duplicate argument filtering outside loop
benpsnyder Feb 28, 2026
600460a
feat(mdx): enhance temporary directory creation with unique identifier
benpsnyder Feb 28, 2026
06bbd3f
refactor(mdx): update start function to use new mdxInternal structure
benpsnyder Feb 28, 2026
4ea0b68
refactor(jsx_renderer): remove unnecessary character handling for braces
benpsnyder Feb 28, 2026
9ff374d
refactor(mdx): streamline statement handling in extractTopLevelStatem…
benpsnyder Feb 28, 2026
2b24a50
feat(mdx): enhance expression handling to support comments and templa…
benpsnyder Feb 28, 2026
e3b97fc
feat(mdx): enhance frontmatter parsing and jsxImportSource handling
benpsnyder Feb 28, 2026
2eecd40
fix(html, mdx): correct port increment logic in server configuration
benpsnyder Feb 28, 2026
1ab504b
fix(bundle): update environment check for Windows to always true
benpsnyder Feb 28, 2026
06e4d7b
feat(jsx_renderer): implement expression restoration and refactor wri…
benpsnyder Feb 28, 2026
8a3b6d4
feat(mdx): implement in-memory MDX compilation and improve import res…
benpsnyder Mar 1, 2026
d76adff
Merge branch 'main' into feat/mdx-support
benpsnyder Mar 1, 2026
68324e2
Merge branch 'main' into feat/mdx-support
benpsnyder Mar 2, 2026
d9f8c68
Merge branch 'main' into feat/mdx-support
benpsnyder Mar 2, 2026
9c9c2a5
Merge branch 'main' into feat/mdx-support
benpsnyder Mar 3, 2026
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
4 changes: 3 additions & 1 deletion packages/bun-native-bundler-plugin-api/bundler_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ typedef enum {
BUN_LOADER_TEXT = 12,
BUN_LOADER_HTML = 17,
BUN_LOADER_YAML = 18,
BUN_LOADER_MD = 20,
BUN_LOADER_MDX = 21,
} BunLoader;

const BunLoader BUN_LOADER_MAX = BUN_LOADER_YAML;
const BunLoader BUN_LOADER_MAX = BUN_LOADER_MDX;

typedef struct BunLogOptions {
size_t __struct_size;
Expand Down
25 changes: 25 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,31 @@ declare module "bun" {
): import("./jsx.d.ts").JSX.Element;
}

/**
* MDX related APIs.
*/
namespace mdx {
interface Options extends markdown.Options {
/**
* Sets the `@jsxImportSource` pragma in the generated JSX.
* Default: `"react"`.
*/
jsxImportSource?: string;
}

/**
* Compile MDX source into a JSX module source string.
*
* @param input MDX source text
* @param options MDX and markdown parser options
* @returns Generated JSX source code
*/
export function compile(
input: string | NodeJS.TypedArray | DataView<ArrayBuffer> | ArrayBufferLike,
options?: Options,
): string;
}

/**
* JSON5 related APIs
*/
Expand Down
1 change: 1 addition & 0 deletions src/api/schema.zig
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ pub const api = struct {
yaml = 19,
json5 = 20,
md = 21,
mdx = 22,
_,

pub fn jsonStringify(self: @This(), writer: anytype) !void {
Expand Down
2 changes: 1 addition & 1 deletion src/bake/DevServer/DirectoryWatchStore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn trackResolutionFailure(store: *DirectoryWatchStore, import_source: []cons
if (!std.fs.path.isAbsolute(import_source)) return;

switch (loader) {
.tsx, .ts, .jsx, .js => {
.tsx, .ts, .jsx, .js, .mdx => {
if (!(bun.strings.startsWith(specifier, "./") or
bun.strings.startsWith(specifier, "../"))) return;
},
Expand Down
4 changes: 2 additions & 2 deletions src/bun.js/ModuleLoader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ pub fn transpileSourceCode(
const disable_transpilying = comptime flags.disableTranspiling();

if (comptime disable_transpilying) {
if (!(loader.isJavaScriptLike() or loader == .toml or loader == .yaml or loader == .json5 or loader == .text or loader == .json or loader == .jsonc)) {
if (!(loader.isJavaScriptLike() or loader == .toml or loader == .yaml or loader == .json5 or loader == .text or loader == .json or loader == .jsonc or loader == .mdx)) {
// Don't print "export default <file path>"
return ResolvedSource{
.allocator = null,
Expand All @@ -112,7 +112,7 @@ pub fn transpileSourceCode(
}

switch (loader) {
.js, .jsx, .ts, .tsx, .json, .jsonc, .toml, .yaml, .json5, .text, .md => {
.js, .jsx, .ts, .tsx, .json, .jsonc, .toml, .yaml, .json5, .text, .md, .mdx => {
// Ensure that if there was an ASTMemoryAllocator in use, it's not used anymore.
var ast_scope = js_ast.ASTMemoryAllocator.Scope{};
ast_scope.enter();
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/api.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub const Terminal = @import("./api/bun/Terminal.zig");
pub const HashObject = @import("./api/HashObject.zig");
pub const JSONCObject = @import("./api/JSONCObject.zig");
pub const MarkdownObject = @import("./api/MarkdownObject.zig");
pub const MdxObject = @import("./api/MdxObject.zig");
pub const TOMLObject = @import("./api/TOMLObject.zig");
pub const UnsafeObject = @import("./api/UnsafeObject.zig");
pub const JSON5Object = @import("./api/JSON5Object.zig");
Expand Down
6 changes: 6 additions & 0 deletions src/bun.js/api/BunObject.zig
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub const BunObject = struct {
pub const SHA512_256 = toJSLazyPropertyCallback(Crypto.SHA512_256.getter);
pub const JSONC = toJSLazyPropertyCallback(Bun.getJSONCObject);
pub const markdown = toJSLazyPropertyCallback(Bun.getMarkdownObject);
pub const mdx = toJSLazyPropertyCallback(Bun.getMdxObject);
pub const TOML = toJSLazyPropertyCallback(Bun.getTOMLObject);
pub const JSON5 = toJSLazyPropertyCallback(Bun.getJSON5Object);
pub const YAML = toJSLazyPropertyCallback(Bun.getYAMLObject);
Expand Down Expand Up @@ -133,6 +134,7 @@ pub const BunObject = struct {
@export(&BunObject.SHA512_256, .{ .name = lazyPropertyCallbackName("SHA512_256") });
@export(&BunObject.JSONC, .{ .name = lazyPropertyCallbackName("JSONC") });
@export(&BunObject.markdown, .{ .name = lazyPropertyCallbackName("markdown") });
@export(&BunObject.mdx, .{ .name = lazyPropertyCallbackName("mdx") });
@export(&BunObject.TOML, .{ .name = lazyPropertyCallbackName("TOML") });
@export(&BunObject.JSON5, .{ .name = lazyPropertyCallbackName("JSON5") });
@export(&BunObject.YAML, .{ .name = lazyPropertyCallbackName("YAML") });
Expand Down Expand Up @@ -1272,6 +1274,9 @@ pub fn getJSONCObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSV
pub fn getMarkdownObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return MarkdownObject.create(globalThis);
}
pub fn getMdxObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return MdxObject.create(globalThis);
}
pub fn getTOMLObject(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
return TOMLObject.create(globalThis);
}
Expand Down Expand Up @@ -2080,6 +2085,7 @@ const HashObject = bun.api.HashObject;
const JSON5Object = bun.api.JSON5Object;
const JSONCObject = bun.api.JSONCObject;
const MarkdownObject = bun.api.MarkdownObject;
const MdxObject = bun.api.MdxObject;
const TOMLObject = bun.api.TOMLObject;
const UnsafeObject = bun.api.UnsafeObject;
const YAMLObject = bun.api.YAMLObject;
Expand Down
105 changes: 105 additions & 0 deletions src/bun.js/api/MdxObject.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
pub fn create(globalThis: *jsc.JSGlobalObject) jsc.JSValue {
const object = JSValue.createEmptyObject(globalThis, 1);
object.put(
globalThis,
bun.String.static("compile"),
jsc.JSFunction.create(globalThis, "compile", compile, 2, .{}),
);
return object;
}

pub fn compile(
globalThis: *jsc.JSGlobalObject,
callframe: *jsc.CallFrame,
) bun.JSError!jsc.JSValue {
const input_value, const opts_value = callframe.argumentsAsArray(2);

if (input_value.isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("Expected a string or buffer to compile", .{});
}

var arena: bun.ArenaAllocator = .init(bun.default_allocator);
defer arena.deinit();

const buffer = try jsc.Node.StringOrBuffer.fromJS(globalThis, arena.allocator(), input_value) orelse {
return globalThis.throwInvalidArguments("Expected a string or buffer to compile", .{});
};
const input = buffer.slice();

const options = try parseOptions(globalThis, arena.allocator(), opts_value);
const result = mdx.compile(input, arena.allocator(), options) catch |err| return switch (err) {
error.OutOfMemory => globalThis.throwOutOfMemory(),
error.JSError, error.JSTerminated => |e| e,
error.StackOverflow => globalThis.throwStackOverflow(),
else => globalThis.throwValue(globalThis.createSyntaxErrorInstance("MDX compile error: {s}", .{@errorName(err)})),
};

return bun.String.createUTF8ForJS(globalThis, result);
}

fn parseOptions(globalThis: *jsc.JSGlobalObject, allocator: std.mem.Allocator, opts_value: JSValue) bun.JSError!mdx.MdxOptions {
var options: mdx.MdxOptions = .{};

if (opts_value.isObject()) {
inline for (@typeInfo(md.Options).@"struct".fields) |field| {
comptime if (field.type != bool) continue;
const camel = comptime camelCaseOf(field.name);
if (try opts_value.getBooleanLoose(globalThis, camel)) |val| {
@field(options.md_options, field.name) = val;
} else if (comptime !std.mem.eql(u8, camel, field.name)) {
if (try opts_value.getBooleanLoose(globalThis, field.name)) |val| {
@field(options.md_options, field.name) = val;
}
}
}

if (try opts_value.get(globalThis, "jsxImportSource")) |import_source_value| {
if (!import_source_value.isUndefinedOrNull()) {
if (!import_source_value.isString()) {
return globalThis.throwInvalidArguments("jsxImportSource must be a string", .{});
}
const str = try import_source_value.toBunString(globalThis);
defer str.deref();
if (!str.isEmpty()) {
const utf8 = str.toUTF8(allocator);
defer utf8.deinit();
options.jsx_import_source = try allocator.dupe(u8, utf8.slice());
}
}
}
}

return options;
}

fn camelCaseOf(comptime snake: []const u8) []const u8 {
return comptime brk: {
var count: usize = 0;
for (snake) |c| {
if (c != '_') count += 1;
}
if (count == snake.len) break :brk snake;

var buf: [count]u8 = undefined;
var i: usize = 0;
var cap_next = false;
for (snake) |c| {
if (c == '_') {
cap_next = true;
} else {
buf[i] = if (cap_next and i != 0 and c >= 'a' and c <= 'z') c - 32 else c;
i += 1;
cap_next = false;
}
}
const final = buf;
break :brk &final;
};
}

const bun = @import("bun");
const jsc = bun.jsc;
const md = bun.md;
const mdx = bun.md.mdx;
const std = @import("std");
const JSValue = jsc.JSValue;
1 change: 1 addition & 0 deletions src/bun.js/bindings/BunObject+exports.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
macro(JSONC) \
macro(MD4) \
macro(markdown) \
macro(mdx) \
macro(MD5) \
macro(S3Client) \
macro(SHA1) \
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
JSON5 BunObject_lazyPropCb_wrap_JSON5 DontDelete|PropertyCallback
JSONL constructJSONLObject ReadOnly|DontDelete|PropertyCallback
markdown BunObject_lazyPropCb_wrap_markdown DontDelete|PropertyCallback
mdx BunObject_lazyPropCb_wrap_mdx DontDelete|PropertyCallback
TOML BunObject_lazyPropCb_wrap_TOML DontDelete|PropertyCallback
YAML BunObject_lazyPropCb_wrap_YAML DontDelete|PropertyCallback
Transpiler BunObject_lazyPropCb_wrap_Transpiler DontDelete|PropertyCallback
Expand Down
4 changes: 3 additions & 1 deletion src/bun.js/bindings/ModuleLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,15 @@ OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC::
loader = BunLoaderTypeYAML;
} else if (loaderString == "md"_s) {
loader = BunLoaderTypeMD;
} else if (loaderString == "mdx"_s) {
loader = BunLoaderTypeMDX;
}
}
}
}

if (loader == BunLoaderTypeNone) [[unlikely]] {
throwException(globalObject, scope, createError(globalObject, "Expected loader to be one of \"js\", \"jsx\", \"object\", \"ts\", \"tsx\", \"toml\", \"yaml\", \"json\", or \"md\""_s));
throwException(globalObject, scope, createError(globalObject, "Expected loader to be one of \"js\", \"jsx\", \"object\", \"ts\", \"tsx\", \"toml\", \"yaml\", \"json\", \"md\", or \"mdx\""_s));
result.value.error = scope.exception();
(void)scope.tryClearException();
return result;
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/bindings/ProcessBindingNatives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ static JSValue processBindingNativesReturnUndefined(VM& vm, JSObject* bindingObj
internal/fs/glob processBindingNativesGetter PropertyCallback
internal/fs/streams processBindingNativesGetter PropertyCallback
internal/html processBindingNativesGetter PropertyCallback
internal/mdx processBindingNativesGetter PropertyCallback
internal/http processBindingNativesGetter PropertyCallback
internal/http/FakeSocket processBindingNativesGetter PropertyCallback
internal/linkedlist processBindingNativesGetter PropertyCallback
Expand Down
3 changes: 2 additions & 1 deletion src/bun.js/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,9 @@ const BunLoaderType BunLoaderTypeJSONC = 8;
const BunLoaderType BunLoaderTypeTOML = 9;
const BunLoaderType BunLoaderTypeWASM = 10;
const BunLoaderType BunLoaderTypeNAPI = 11;
const BunLoaderType BunLoaderTypeYAML = 19;
const BunLoaderType BunLoaderTypeYAML = 18;
const BunLoaderType BunLoaderTypeMD = 20;
const BunLoaderType BunLoaderTypeMDX = 21;

#pragma mark - Stream

Expand Down
2 changes: 1 addition & 1 deletion src/bundler/LinkerContext.zig
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ pub const LinkerContext = struct {
const loader = loaders[record.source_index.get()];

switch (loader) {
.jsx, .js, .ts, .tsx, .napi, .sqlite, .json, .jsonc, .json5, .yaml, .html, .sqlite_embedded, .md => {
.jsx, .js, .ts, .tsx, .napi, .sqlite, .json, .jsonc, .json5, .yaml, .html, .sqlite_embedded, .md, .mdx => {
log.addErrorFmt(
source,
record.range.loc,
Expand Down
40 changes: 39 additions & 1 deletion src/bundler/ParseTask.zig
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,43 @@ fn getAST(
ast.addUrlForCss(allocator, source, "text/html", null);
return ast;
},
.mdx => {
const jsx_source = bun.md.mdx.compile(source.contents, allocator, .{}) catch {
log.addError(
source,
Logger.Loc.Empty,
"Failed to compile MDX",
) catch |err| bun.handleOom(err);
return error.ParserError;
};

var virtual_source = source.*;
virtual_source.contents = jsx_source;

var jsx_opts = opts;
jsx_opts.ts = true;
jsx_opts.jsx.parse = true;
jsx_opts.features.react_fast_refresh = transpiler.options.react_fast_refresh and !source.path.isNodeModule();

return if (try resolver.caches.js.parse(
transpiler.allocator,
jsx_opts,
transpiler.options.define,
log,
&virtual_source,
)) |res|
JSAst.init(res.ast)
else switch (jsx_opts.module_type == .esm) {
inline else => |as_undefined| try getEmptyAST(
log,
transpiler,
jsx_opts,
allocator,
source,
if (as_undefined) E.Undefined else E.Object,
),
};
},

.sqlite_embedded, .sqlite => {
if (!transpiler.options.target.isBun()) {
Expand Down Expand Up @@ -1196,6 +1233,7 @@ fn runWithSourceCode(

const output_format = transpiler.options.output_format;

task.jsx.parse = loader.isJSX() or loader == .mdx;
var opts = js_parser.Parser.Options.init(task.jsx, loader);
opts.bundle = true;
opts.warn_about_unbundled_modules = false;
Expand Down Expand Up @@ -1252,7 +1290,7 @@ fn runWithSourceCode(
opts.code_splitting = transpiler.options.code_splitting;
opts.module_type = task.module_type;

task.jsx.parse = loader.isJSX();
task.jsx.parse = loader.isJSX() or loader == .mdx;

var unique_key_for_additional_file: FileLoaderHash = .{
.key = "",
Expand Down
16 changes: 14 additions & 2 deletions src/cli/run_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1319,7 +1319,8 @@ pub const RunCommand = struct {
};
}

_ = _bootAndHandleError(ctx, absolute_script_path.?, null);
const fast_run_loader: ?bun.options.Loader = if (strings.hasSuffixComptime(absolute_script_path.?, ".mdx")) .html else null;
_ = _bootAndHandleError(ctx, absolute_script_path.?, fast_run_loader);
return true;
}
pub fn exec(
Expand All @@ -1336,7 +1337,8 @@ pub const RunCommand = struct {
// find what to run

var positionals = ctx.positionals;
if (positionals.len > 0 and strings.eqlComptime(positionals[0], "run")) {
const is_run_subcommand = positionals.len > 0 and strings.eqlComptime(positionals[0], "run");
if (is_run_subcommand) {
positionals = positionals[1..];
}

Expand Down Expand Up @@ -1519,6 +1521,11 @@ pub const RunCommand = struct {
var resolved_mutable = resolved;
const path = resolved_mutable.path().?;
const loader: bun.options.Loader = this_transpiler.options.loaders.get(path.name.ext) orelse .tsx;
if (loader == .mdx) {
log("Resolved to MDX direct-entry mode: `{s}`", .{path.text});
return _bootAndHandleError(ctx, path.text, .html);
}

if (loader.canBeRunByBun() or loader == .html) {
log("Resolved to: `{s}`", .{path.text});
return _bootAndHandleError(ctx, path.text, loader);
Expand All @@ -1533,6 +1540,11 @@ pub const RunCommand = struct {
return _bootAndHandleError(ctx, target_name, .html);
}
}
if (strings.hasSuffixComptime(target_name, ".mdx")) {
if (strings.containsChar(target_name, '*')) {
return _bootAndHandleError(ctx, target_name, .html);
}
}
}

// execute a node_modules/.bin/<X> command, or (run only) a system command like 'ls'
Expand Down
Loading