diff --git a/src/ast/parseStmt.zig b/src/ast/parseStmt.zig index b8bce67462..8f946e079b 100644 --- a/src/ast/parseStmt.zig +++ b/src/ast/parseStmt.zig @@ -1223,8 +1223,9 @@ pub fn ParseStmt( // "module Foo {}" // "declare module 'fs' {}" // "declare module 'fs';" - if (((opts.is_module_scope or opts.is_namespace_scope) and (p.lexer.token == .t_identifier or - (p.lexer.token == .t_string_literal and opts.is_typescript_declare)))) + if (!p.lexer.has_newline_before and + (opts.is_module_scope or opts.is_namespace_scope) and + (p.lexer.token == .t_identifier or (p.lexer.token == .t_string_literal and opts.is_typescript_declare))) { return p.parseTypeScriptNamespaceStmt(loc, opts); } diff --git a/test/regression/issue/22929-module-extensions-asi.test.ts b/test/regression/issue/22929-module-extensions-asi.test.ts new file mode 100644 index 0000000000..7c5ecf31d8 --- /dev/null +++ b/test/regression/issue/22929-module-extensions-asi.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync, writeFileSync } from "fs"; +import { bunEnv, bunExe } from "harness"; +import { tmpdir } from "os"; +import { join } from "path"; + +test("Module._extensions should not break ASI (automatic semicolon insertion)", async () => { + const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-asi-")); + + // Create a module without semicolons that relies on ASI + const moduleWithoutSemi = join(dir, "module-no-semi.js"); + writeFileSync( + moduleWithoutSemi, + `function f() {} +module.exports = f +f.f = f`, + ); + + // Create a test file that hooks Module._extensions + const testFile = join(dir, "test.js"); + writeFileSync( + testFile, + ` +const Module = require("module"); +const orig = Module._extensions[".js"]; + +// Hook Module._extensions[".js"] - commonly done by transpiler libraries +Module._extensions[".js"] = (m, f) => { + return orig(m, f); +}; + +// This should work without parse errors +const result = require("./module-no-semi.js"); +if (typeof result !== 'function') { + throw new Error('Expected function but got ' + typeof result); +} +if (result.f !== result) { + throw new Error('Expected result.f === result'); +} +console.log('SUCCESS'); +`, + ); + + // Run the test + const proc = Bun.spawn({ + cmd: [bunExe(), testFile], + cwd: dir, + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should not have parse errors + expect(stderr).not.toContain("Expected '{'"); + expect(stderr).not.toContain("Unexpected end of file"); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("SUCCESS"); +}); + +test("Module._extensions works with modules that have semicolons", async () => { + const dir = mkdtempSync(join(tmpdir(), "bun-module-extensions-semi-")); + + // Create a module with semicolons + const moduleWithSemi = join(dir, "module-with-semi.js"); + writeFileSync( + moduleWithSemi, + `function g() { return 42; } +module.exports = g; +g.g = g;`, + ); + + // Create a test file that hooks Module._extensions + const testFile = join(dir, "test.js"); + writeFileSync( + testFile, + ` +const Module = require("module"); +const orig = Module._extensions[".js"]; + +Module._extensions[".js"] = (m, f) => { + return orig(m, f); +}; + +// This should also work with semicolons +const result = require("./module-with-semi.js"); +if (typeof result !== 'function') { + throw new Error('Expected function but got ' + typeof result); +} +if (result() !== 42) { + throw new Error('Expected result() === 42'); +} +if (result.g !== result) { + throw new Error('Expected result.g === result'); +} +console.log('SUCCESS'); +`, + ); + + // Run the test + const proc = Bun.spawn({ + cmd: [bunExe(), testFile], + cwd: dir, + env: bunEnv, + stderr: "pipe", + stdout: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // Should work correctly + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe("SUCCESS"); +});