Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
2c79dde82e fix(plugins): onLoad returning CJS contents works with require()
When a plugin's onLoad handler returned CommonJS-style contents
(module.exports), the transpiler correctly detected CJS and wrapped
the code, but handleVirtualModuleResult always routed through the ESM
loader, ignoring the isCommonJSModule flag. The CJS wrapper function
was loaded as ESM where it was never called, resulting in empty exports.

Now check isCommonJSModule after transpiling virtual module contents
and evaluate through the CJS path when require() is the consumer.

Closes #27799

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 19:53:34 +00:00
4 changed files with 280 additions and 83 deletions

View File

@@ -4,87 +4,227 @@ if(NOT BUILDKITE_CACHE OR NOT BUN_LINK_ONLY)
return()
endif()
# This runs inside the ${targetKey}-build-bun step (see .buildkite/ci.mjs), which
# links artifacts from ${targetKey}-build-cpp and ${targetKey}-build-zig in the
# same build. Step keys follow a fixed ${targetKey}-build-{cpp,zig,bun} pattern
# and build-bun's depends_on lists exactly those two steps, so we derive the
# siblings by swapping the suffix on our own BUILDKITE_STEP_KEY.
optionx(BUILDKITE_ORGANIZATION_SLUG STRING "The organization slug to use on Buildkite" DEFAULT "bun")
optionx(BUILDKITE_PIPELINE_SLUG STRING "The pipeline slug to use on Buildkite" DEFAULT "bun")
optionx(BUILDKITE_BUILD_ID STRING "The build ID (UUID) to use on Buildkite")
optionx(BUILDKITE_BUILD_NUMBER STRING "The build number to use on Buildkite")
optionx(BUILDKITE_GROUP_ID STRING "The group ID to use on Buildkite")
if(NOT DEFINED ENV{BUILDKITE_STEP_KEY})
message(FATAL_ERROR "BUILDKITE_STEP_KEY is not set (expected inside a Buildkite job)")
if(ENABLE_BASELINE)
set(DEFAULT_BUILDKITE_GROUP_KEY ${OS}-${ARCH}-baseline)
else()
set(DEFAULT_BUILDKITE_GROUP_KEY ${OS}-${ARCH})
endif()
set(BUILDKITE_STEP_KEY $ENV{BUILDKITE_STEP_KEY})
if(NOT BUILDKITE_STEP_KEY MATCHES "^(.+)-build-bun$")
message(FATAL_ERROR "Unexpected BUILDKITE_STEP_KEY '${BUILDKITE_STEP_KEY}' (expected '<target>-build-bun')")
optionx(BUILDKITE_GROUP_KEY STRING "The group key to use on Buildkite" DEFAULT ${DEFAULT_BUILDKITE_GROUP_KEY})
if(BUILDKITE)
optionx(BUILDKITE_BUILD_ID_OVERRIDE STRING "The build ID to use on Buildkite")
if(BUILDKITE_BUILD_ID_OVERRIDE)
setx(BUILDKITE_BUILD_ID ${BUILDKITE_BUILD_ID_OVERRIDE})
endif()
endif()
set(BUILDKITE_TARGET_KEY ${CMAKE_MATCH_1})
# Download all artifacts from both sibling steps. The agent scopes to the
# current build via $BUILDKITE_BUILD_ID; --step resolves by key within it.
# git clean -ffxdq runs between builds (BUILDKITE_GIT_CLEAN_FLAGS), so
# ${BUILD_PATH} starts clean.
set(BUILDKITE_PATH ${BUILD_PATH}/buildkite)
set(BUILDKITE_BUILDS_PATH ${BUILDKITE_PATH}/builds)
file(MAKE_DIRECTORY ${BUILD_PATH})
if(NOT BUILDKITE_BUILD_ID)
# TODO: find the latest build on the main branch that passed
return()
endif()
foreach(SUFFIX cpp zig)
set(STEP ${BUILDKITE_TARGET_KEY}-build-${SUFFIX})
message(STATUS "Downloading artifacts from ${STEP}")
execute_process(
COMMAND buildkite-agent artifact download * . --step ${STEP}
WORKING_DIRECTORY ${BUILD_PATH}
RESULT_VARIABLE DOWNLOAD_RC
ERROR_VARIABLE DOWNLOAD_ERR
)
if(NOT DOWNLOAD_RC EQUAL 0)
message(FATAL_ERROR "buildkite-agent artifact download from ${STEP} failed: ${DOWNLOAD_ERR}")
endif()
endforeach()
# Use BUILDKITE_BUILD_NUMBER for the URL if available, as the UUID format causes a 302 redirect
# that CMake's file(DOWNLOAD) doesn't follow, resulting in empty response.
if(BUILDKITE_BUILD_NUMBER)
setx(BUILDKITE_BUILD_URL https://buildkite.com/${BUILDKITE_ORGANIZATION_SLUG}/${BUILDKITE_PIPELINE_SLUG}/builds/${BUILDKITE_BUILD_NUMBER})
else()
setx(BUILDKITE_BUILD_URL https://buildkite.com/${BUILDKITE_ORGANIZATION_SLUG}/${BUILDKITE_PIPELINE_SLUG}/builds/${BUILDKITE_BUILD_ID})
endif()
setx(BUILDKITE_BUILD_PATH ${BUILDKITE_BUILDS_PATH}/builds/${BUILDKITE_BUILD_ID})
# libbun-profile.a and libbun-asan.a are gzipped before upload (see
# register_command's ARTIFACTS handling in Globals.cmake). Windows .lib
# files are not gzipped, so this glob is empty there.
file(GLOB BUILDKITE_GZ_ARTIFACTS "${BUILD_PATH}/*.gz")
foreach(GZ ${BUILDKITE_GZ_ARTIFACTS})
message(STATUS "Unpacking ${GZ}")
execute_process(
COMMAND gunzip -f ${GZ}
RESULT_VARIABLE GUNZIP_RC
ERROR_VARIABLE GUNZIP_ERR
)
if(NOT GUNZIP_RC EQUAL 0)
message(FATAL_ERROR "gunzip ${GZ} failed: ${GUNZIP_ERR}")
endif()
endforeach()
# Artifacts are uploaded with subdirectory paths (lolhtml/release/liblolhtml.a,
# mimalloc/CMakeFiles/mimalloc-obj.dir/src/static.c.o, etc.) and the agent
# recreates that structure on download. Recurse, but skip top-level CMakeFiles/
# (our own compiler detection) and cache/ — nested CMakeFiles/ are real artifacts.
file(GLOB_RECURSE BUILDKITE_LINK_ARTIFACTS
"${BUILD_PATH}/*.o"
"${BUILD_PATH}/*.a"
"${BUILD_PATH}/*.lib"
file(
DOWNLOAD ${BUILDKITE_BUILD_URL}
HTTPHEADER "Accept: application/json"
TIMEOUT 15
STATUS BUILDKITE_BUILD_STATUS
${BUILDKITE_BUILD_PATH}/build.json
)
string(REGEX REPLACE "\\." "\\\\." BUILD_PATH_RE "${BUILD_PATH}")
list(FILTER BUILDKITE_LINK_ARTIFACTS EXCLUDE REGEX "^${BUILD_PATH_RE}/(CMakeFiles|cache)/")
if(NOT BUILDKITE_LINK_ARTIFACTS)
message(FATAL_ERROR "No linkable artifacts found in ${BUILD_PATH} after download")
if(NOT BUILDKITE_BUILD_STATUS EQUAL 0)
message(FATAL_ERROR "No build found: ${BUILDKITE_BUILD_STATUS} ${BUILDKITE_BUILD_URL}")
return()
endif()
list(LENGTH BUILDKITE_LINK_ARTIFACTS BUILDKITE_LINK_COUNT)
message(STATUS "Registered ${BUILDKITE_LINK_COUNT} linkable artifacts from ${BUILDKITE_TARGET_KEY}-build-{cpp,zig}")
# Register a no-op custom command for each linkable artifact so register_command
# (Globals.cmake) sees them as GENERATED and shims its own output to
# ${output}.always_run_${target} instead of overwriting them with a rebuild.
# With no DEPENDS, the no-op never fires — the file already exists.
file(READ ${BUILDKITE_BUILD_PATH}/build.json BUILDKITE_BUILD)
# CMake's string(JSON ...) interprets escape sequences like \n, \r, \t.
# We need to escape these specific sequences while preserving valid JSON escapes like \" and \\.
# Strategy: Use a unique placeholder to protect \\ sequences, escape \n/\r/\t, then restore \\.
# This prevents \\n (literal backslash + n) from being corrupted to \\\n.
set(BKSLASH_PLACEHOLDER "___BKSLASH_PLACEHOLDER_7f3a9b2c___")
string(REPLACE "\\\\" "${BKSLASH_PLACEHOLDER}" BUILDKITE_BUILD "${BUILDKITE_BUILD}")
string(REPLACE "\\n" "\\\\n" BUILDKITE_BUILD "${BUILDKITE_BUILD}")
string(REPLACE "\\r" "\\\\r" BUILDKITE_BUILD "${BUILDKITE_BUILD}")
string(REPLACE "\\t" "\\\\t" BUILDKITE_BUILD "${BUILDKITE_BUILD}")
string(REPLACE "${BKSLASH_PLACEHOLDER}" "\\\\" BUILDKITE_BUILD "${BUILDKITE_BUILD}")
# CMake treats semicolons as list separators in unquoted variable expansions.
# Commit messages and other JSON string fields can contain semicolons, which would
# cause string(JSON) to receive garbled arguments. Escape them before parsing.
string(REPLACE ";" "\\;" BUILDKITE_BUILD "${BUILDKITE_BUILD}")
foreach(ARTIFACT ${BUILDKITE_LINK_ARTIFACTS})
add_custom_command(
OUTPUT ${ARTIFACT}
COMMAND ${CMAKE_COMMAND} -E true
string(JSON BUILDKITE_BUILD_UUID GET "${BUILDKITE_BUILD}" id)
string(JSON BUILDKITE_JOBS GET "${BUILDKITE_BUILD}" jobs)
string(JSON BUILDKITE_JOBS_COUNT LENGTH "${BUILDKITE_JOBS}")
if(NOT BUILDKITE_JOBS_COUNT GREATER 0)
message(FATAL_ERROR "No jobs found: ${BUILDKITE_BUILD_URL}")
return()
endif()
set(BUILDKITE_JOBS_FAILED)
set(BUILDKITE_JOBS_NOT_FOUND)
set(BUILDKITE_JOBS_NO_ARTIFACTS)
set(BUILDKITE_JOBS_NO_MATCH)
set(BUILDKITE_JOBS_MATCH)
math(EXPR BUILDKITE_JOBS_MAX_INDEX "${BUILDKITE_JOBS_COUNT} - 1")
foreach(i RANGE ${BUILDKITE_JOBS_MAX_INDEX})
string(JSON BUILDKITE_JOB GET "${BUILDKITE_JOBS}" ${i})
string(JSON BUILDKITE_JOB_ID GET "${BUILDKITE_JOB}" id)
string(JSON BUILDKITE_JOB_PASSED GET "${BUILDKITE_JOB}" passed)
string(JSON BUILDKITE_JOB_GROUP_ID GET "${BUILDKITE_JOB}" group_uuid)
string(JSON BUILDKITE_JOB_GROUP_KEY GET "${BUILDKITE_JOB}" group_identifier)
string(JSON BUILDKITE_JOB_NAME GET "${BUILDKITE_JOB}" step_key)
if(NOT BUILDKITE_JOB_NAME)
string(JSON BUILDKITE_JOB_NAME GET "${BUILDKITE_JOB}" name)
endif()
if(NOT BUILDKITE_JOB_PASSED)
list(APPEND BUILDKITE_JOBS_FAILED ${BUILDKITE_JOB_NAME})
continue()
endif()
if(NOT (BUILDKITE_GROUP_ID AND BUILDKITE_GROUP_ID STREQUAL BUILDKITE_JOB_GROUP_ID) AND
NOT (BUILDKITE_GROUP_KEY AND BUILDKITE_GROUP_KEY STREQUAL BUILDKITE_JOB_GROUP_KEY))
list(APPEND BUILDKITE_JOBS_NO_MATCH ${BUILDKITE_JOB_NAME})
continue()
endif()
set(BUILDKITE_ARTIFACTS_URL https://buildkite.com/organizations/${BUILDKITE_ORGANIZATION_SLUG}/pipelines/${BUILDKITE_PIPELINE_SLUG}/builds/${BUILDKITE_BUILD_UUID}/jobs/${BUILDKITE_JOB_ID}/artifacts)
set(BUILDKITE_ARTIFACTS_PATH ${BUILDKITE_BUILD_PATH}/artifacts/${BUILDKITE_JOB_ID}.json)
file(
DOWNLOAD ${BUILDKITE_ARTIFACTS_URL}
HTTPHEADER "Accept: application/json"
TIMEOUT 15
STATUS BUILDKITE_ARTIFACTS_STATUS
${BUILDKITE_ARTIFACTS_PATH}
)
if(NOT BUILDKITE_ARTIFACTS_STATUS EQUAL 0)
list(APPEND BUILDKITE_JOBS_NOT_FOUND ${BUILDKITE_JOB_NAME})
continue()
endif()
file(READ ${BUILDKITE_ARTIFACTS_PATH} BUILDKITE_ARTIFACTS)
string(REPLACE ";" "\\;" BUILDKITE_ARTIFACTS "${BUILDKITE_ARTIFACTS}")
string(JSON BUILDKITE_ARTIFACTS_LENGTH LENGTH "${BUILDKITE_ARTIFACTS}")
if(NOT BUILDKITE_ARTIFACTS_LENGTH GREATER 0)
list(APPEND BUILDKITE_JOBS_NO_ARTIFACTS ${BUILDKITE_JOB_NAME})
continue()
endif()
math(EXPR BUILDKITE_ARTIFACTS_MAX_INDEX "${BUILDKITE_ARTIFACTS_LENGTH} - 1")
foreach(i RANGE 0 ${BUILDKITE_ARTIFACTS_MAX_INDEX})
string(JSON BUILDKITE_ARTIFACT GET "${BUILDKITE_ARTIFACTS}" ${i})
string(JSON BUILDKITE_ARTIFACT_ID GET "${BUILDKITE_ARTIFACT}" id)
string(JSON BUILDKITE_ARTIFACT_PATH GET "${BUILDKITE_ARTIFACT}" path)
if(NOT BUILDKITE_ARTIFACT_PATH MATCHES "\\.(o|a|lib|zip|tar|gz)")
continue()
endif()
if(BUILDKITE)
if(BUILDKITE_ARTIFACT_PATH STREQUAL "libbun-profile.a")
set(BUILDKITE_ARTIFACT_PATH libbun-profile.a.gz)
elseif(BUILDKITE_ARTIFACT_PATH STREQUAL "libbun-asan.a")
set(BUILDKITE_ARTIFACT_PATH libbun-asan.a.gz)
endif()
set(BUILDKITE_DOWNLOAD_COMMAND buildkite-agent artifact download ${BUILDKITE_ARTIFACT_PATH} . --build ${BUILDKITE_BUILD_UUID} --step ${BUILDKITE_JOB_ID})
else()
set(BUILDKITE_DOWNLOAD_COMMAND curl -L -o ${BUILDKITE_ARTIFACT_PATH} ${BUILDKITE_ARTIFACTS_URL}/${BUILDKITE_ARTIFACT_ID})
endif()
add_custom_command(
COMMENT
"Downloading ${BUILDKITE_ARTIFACT_PATH}"
VERBATIM COMMAND
${BUILDKITE_DOWNLOAD_COMMAND}
WORKING_DIRECTORY
${BUILD_PATH}
OUTPUT
${BUILD_PATH}/${BUILDKITE_ARTIFACT_PATH}
)
if(BUILDKITE_ARTIFACT_PATH STREQUAL "libbun-profile.a.gz")
add_custom_command(
COMMENT
"Unpacking libbun-profile.a.gz"
VERBATIM COMMAND
gunzip libbun-profile.a.gz
WORKING_DIRECTORY
${BUILD_PATH}
OUTPUT
${BUILD_PATH}/libbun-profile.a
DEPENDS
${BUILD_PATH}/libbun-profile.a.gz
)
elseif(BUILDKITE_ARTIFACT_PATH STREQUAL "libbun-asan.a.gz")
add_custom_command(
COMMENT
"Unpacking libbun-asan.a.gz"
VERBATIM COMMAND
gunzip libbun-asan.a.gz
WORKING_DIRECTORY
${BUILD_PATH}
OUTPUT
${BUILD_PATH}/libbun-asan.a
DEPENDS
${BUILD_PATH}/libbun-asan.a.gz
)
endif()
endforeach()
list(APPEND BUILDKITE_JOBS_MATCH ${BUILDKITE_JOB_NAME})
endforeach()
if(BUILDKITE_JOBS_FAILED)
list(SORT BUILDKITE_JOBS_FAILED COMPARE STRING)
list(JOIN BUILDKITE_JOBS_FAILED " " BUILDKITE_JOBS_FAILED)
message(WARNING "The following jobs were found, but failed: ${BUILDKITE_JOBS_FAILED}")
endif()
if(BUILDKITE_JOBS_NOT_FOUND)
list(SORT BUILDKITE_JOBS_NOT_FOUND COMPARE STRING)
list(JOIN BUILDKITE_JOBS_NOT_FOUND " " BUILDKITE_JOBS_NOT_FOUND)
message(WARNING "The following jobs were found, but could not fetch their data: ${BUILDKITE_JOBS_NOT_FOUND}")
endif()
if(BUILDKITE_JOBS_NO_MATCH)
list(SORT BUILDKITE_JOBS_NO_MATCH COMPARE STRING)
list(JOIN BUILDKITE_JOBS_NO_MATCH " " BUILDKITE_JOBS_NO_MATCH)
message(WARNING "The following jobs were found, but did not match the group ID: ${BUILDKITE_JOBS_NO_MATCH}")
endif()
if(BUILDKITE_JOBS_NO_ARTIFACTS)
list(SORT BUILDKITE_JOBS_NO_ARTIFACTS COMPARE STRING)
list(JOIN BUILDKITE_JOBS_NO_ARTIFACTS " " BUILDKITE_JOBS_NO_ARTIFACTS)
message(WARNING "The following jobs were found, but had no artifacts: ${BUILDKITE_JOBS_NO_ARTIFACTS}")
endif()
if(BUILDKITE_JOBS_MATCH)
list(SORT BUILDKITE_JOBS_MATCH COMPARE STRING)
list(JOIN BUILDKITE_JOBS_MATCH " " BUILDKITE_JOBS_MATCH)
message(STATUS "The following jobs were found, and matched the group ID: ${BUILDKITE_JOBS_MATCH}")
endif()
if(NOT BUILDKITE_JOBS_FAILED AND NOT BUILDKITE_JOBS_NOT_FOUND AND NOT BUILDKITE_JOBS_NO_MATCH AND NOT BUILDKITE_JOBS_NO_ARTIFACTS AND NOT BUILDKITE_JOBS_MATCH)
message(FATAL_ERROR "Something went wrong with Buildkite?")
endif()

View File

@@ -124,30 +124,30 @@ Becomes something like this:
To use React in your client-side code, import `react-dom/client` and render your app.
<CodeGroup>
```ts title="src/backend.ts" icon="/icons/typescript.svg"
import dashboard from "../public/dashboard.html";
import { serve } from "bun";
serve({
routes: {
"/": dashboard,
},
async fetch(req) {
// ...api requests
return new Response("hello world");
},
routes: {
"/": dashboard,
},
async fetch(req) {
// ...api requests
return new Response("hello world");
},
});
```
````
```tsx title="src/frontend.tsx" icon="/icons/typescript.svg"
import { createRoot } from "react-dom/client";
import App from "./app";
import { createRoot } from 'react-dom/client';
import App from './app';
const container = document.getElementById("root");
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
```
````
```html title="public/dashboard.html" icon="file-code"
<!DOCTYPE html>

View File

@@ -390,6 +390,13 @@ static JSValue handleVirtualModuleResult(
RELEASE_AND_RETURN(scope, reject(JSValue::decode(res->result.err.value)));
}
if (commonJSModule && res->result.value.isCommonJSModule) {
auto specifierString = specifier->toWTFString(BunString::ZeroCopy);
commonJSModule->evaluate(globalObject, specifierString, res->result.value);
RETURN_IF_EXCEPTION(scope, {});
return commonJSModule;
}
auto provider = Zig::SourceProvider::create(globalObject, res->result.value);
return resolve(JSC::JSSourceCode::create(vm, JSC::SourceCode(provider)));
}

View File

@@ -0,0 +1,50 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("onLoad plugin returning CJS contents works with require()", async () => {
using dir = tempDir("issue-27799", {
"plugin.js": `
Bun.plugin({
name: "cjs-patch",
setup(build) {
build.onLoad({ filter: /tiny-module\\.js$/ }, () => {
return {
loader: "js",
contents: \`
module.exports.greet = function greet(name) { return "patched hello " + name; }
module.exports.add = function add(a, b) { return a + b + a; }
\`,
};
});
},
});
`,
"tiny-module.js": `
module.exports.greet = function greet(name) {
return "hello " + name;
}
module.exports.add = function add(a, b) {
return a + b;
}
`,
"run-demo.js": `
const { greet, add } = require('./tiny-module');
console.log(greet('world'));
console.log(add(1, 2));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload=./plugin.js", "run-demo.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("patched hello world\n4\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});