Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a30d31e234 fix(transpiler): preserve "use strict" directives in function bodies
The CJS transpiler was stripping "use strict" directives from function
bodies, causing functions to run in sloppy mode. This broke libraries
like json-logic-js that use UMD patterns with "use strict" inside
factory functions.

Two issues were fixed:
1. The parser unconditionally skipped "use strict" during directive
   prologue parsing. Now it only skips at module scope (where it's
   restored during CJS wrapping) and converts function-level directives
   to S.Directive nodes.
2. The joinWithComma optimization pass (minify_syntax + dead_code_elimination)
   unconditionally removed all S.Directive nodes. Now it preserves them
   since they have semantic meaning.

Closes #19490

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 11:21:55 +00:00
3 changed files with 142 additions and 4 deletions

View File

@@ -1192,11 +1192,21 @@ pub fn Parse(
isDirectivePrologue = true;
if (str.eqlComptime("use strict")) {
skip = true;
// Track "use strict" directives
p.current_scope.strict_mode = .explicit_strict_mode;
if (p.current_scope == p.module_scope)
if (p.current_scope == p.module_scope) {
// Module-level "use strict" is restored later
// during CJS wrapping (see P.zig), so skip it here.
skip = true;
p.module_scope_directive_loc = stmt.loc;
} else {
// Function-level "use strict" must be preserved
// in the output to maintain correct strict mode
// semantics (e.g. `this` boxing behavior).
stmt = Stmt.alloc(S.Directive, S.Directive{
.value = str.slice(p.allocator),
}, stmt.loc);
}
} else if (str.eqlComptime("use asm")) {
skip = true;
stmt.data = Prefill.Data.SEmpty;

View File

@@ -1192,8 +1192,12 @@ pub fn Visit(
switch (stmt.data) {
.s_empty => continue,
// skip directives for now
.s_directive => continue,
// Preserve directives (e.g. "use strict") as they have
// semantic meaning and must not be removed.
.s_directive => {
output.append(stmt) catch unreachable;
continue;
},
.s_local => |local| {
// Merge adjacent local statements

View File

@@ -0,0 +1,124 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("use strict in function body is preserved in CJS transpilation", async () => {
using dir = tempDir("issue-19490", {
"strict_module.js": `
;(function(root, factory) {
if (typeof exports === "object") {
module.exports = factory();
}
}(this, function() {
"use strict";
var fn = function() { return typeof this; };
return { fn: fn };
}));
`,
"test.js": `
var mod = require("./strict_module.js");
var result = mod.fn.apply("hello");
console.log(result);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// In strict mode, `this` is not boxed, so typeof this === "string"
// In sloppy mode, `this` is boxed to String object, so typeof this === "object"
expect(stdout.trim()).toBe("string");
expect(exitCode).toBe(0);
});
test("json-logic-js style UMD with use strict works correctly", async () => {
using dir = tempDir("issue-19490-umd", {
"logic.js": `
;(function(root, factory) {
if (typeof exports === "object") {
module.exports = factory();
}
}(this, function() {
"use strict";
var arrayUnique = function(array) {
var a = [];
for (var i = 0, l = array.length; i < l; i++) {
if (a.indexOf(array[i]) === -1) {
a.push(array[i]);
}
}
return a;
};
var operations = {
"some": function(data, values) {
var dominated = values[0];
var rule = values[1];
for (var i = 0; i < dominated.length; i++) {
// In strict mode, apply with a string keeps this as a string
// In sloppy mode, it gets boxed to a String object
if (applyRule.apply(dominated[i], [rule])) {
return true;
}
}
return false;
}
};
function applyRule(rule) {
if (typeof rule === "object" && rule !== null && "===" in rule) {
var args = rule["==="];
var left = args[0];
var right = args[1];
// When left is { var: "" }, resolve to \`this\`
if (typeof left === "object" && "var" in left && left["var"] === "") {
left = this;
}
return left === right;
}
return false;
}
return {
apply: function(rules) {
if (typeof rules === "object" && rules !== null && "some" in rules) {
return operations["some"](null, rules["some"]);
}
return null;
}
};
}));
`,
"test.js": `
var logic = require("./logic.js");
var result = logic.apply({
some: [
["hello", "world"],
{ "===": [{ var: "" }, "hello"] }
]
});
console.log(result);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "test.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("true");
expect(exitCode).toBe(0);
});