Rows: ...DOM nodes: ...Delegated listeners: 1No line selected
4
src/install/PackageManager/PackageJSONEditor.zig
@@ -459,7 +459,9 @@ pub fn edit( | ||
| 459 | 459 | } |
| 460 | 460 | break; |
| 461 | 461 | } else { |
| 462 | - if (request.version.tag == .github or request.version.tag == .git) { | |
| 462 | + if (request.e_string == null and !request.is_aliased and | |
| 463 | + (request.version.tag == .github or request.version.tag == .git or request.version.tag == .tarball or request.version.tag == .folder or request.version.tag == .symlink)) | |
| 464 | + { | |
| 463 | 465 | for (query.expr.data.e_object.properties.slice()) |item| { |
| 464 | 466 | if (item.value) |v| { |
| 465 | 467 | const url = request.version.literal.slice(request.version_buf); |
4
src/js/node/events.ts
@@ -436,7 +436,9 @@ EventEmitterPrototype.eventNames = function eventNames() { | ||
| 436 | 436 | |
| 437 | 437 | EventEmitterPrototype[kCapture] = false; |
| 438 | 438 | |
| 439 | -function once(emitter, type, options = kEmptyObject) { | |
| 439 | +// `async` so validation/already-aborted `throw`s surface as rejected | |
| 440 | +// promise instead of synchronous throw — matches Node's `events.once`. | |
| 441 | +async function once(emitter, type, options = kEmptyObject) { | |
| 440 | 442 | validateObject(options, "options"); |
| 441 | 443 | var signal = options?.signal; |
| 442 | 444 | validateAbortSignal(signal, "options.signal"); |
2
src/js_parser/ast/parseProperty.zig
@@ -318,7 +318,7 @@ pub fn ParseProperty( | ||
| 318 | 318 | }, |
| 319 | 319 | } |
| 320 | 320 | } |
| 321 | - } else if (p.lexer.token == .t_open_brace and strings.eqlComptime(name, "static")) { | |
| 321 | + } else if (opts.is_class and p.lexer.token == .t_open_brace and strings.eqlComptime(name, "static")) { | |
| 322 | 322 | const loc = p.lexer.loc(); |
| 323 | 323 | try p.lexer.next(); |
| 324 | 324 | |
2
src/js_parser/lexer.zig
@@ -1778,6 +1778,7 @@ fn NewLexer_( | ||
| 1778 | 1778 | |
| 1779 | 1779 | lexer.end = lexer.current; |
| 1780 | 1780 | lexer.token = T.t_syntax_error; |
| 1781 | + lexer.step(); | |
| 1781 | 1782 | }, |
| 1782 | 1783 | } |
| 1783 | 1784 | |
@@ -2318,6 +2319,7 @@ fn NewLexer_( | ||
| 2318 | 2319 | |
| 2319 | 2320 | lexer.end = lexer.current; |
| 2320 | 2321 | lexer.token = .t_syntax_error; |
| 2322 | + lexer.step(); | |
| 2321 | 2323 | }, |
| 2322 | 2324 | } |
| 2323 | 2325 | |
20
test/bundler/bundler_naming.test.ts
@@ -302,4 +302,24 @@ describe("bundler", () => { | ||
| 302 | 302 | }, |
| 303 | 303 | ], |
| 304 | 304 | }); |
| 305 | + // A non-ASCII basename char must collapse to a single "_" in the generated | |
| 306 | + // CommonJS wrapper symbol, not one "_" per UTF-8 byte. Regressed in the | |
| 307 | + // Rust port; Zig's identifier formatter already decodes multi-byte codepoints. | |
| 308 | + itBundled("naming/NonAsciiSourceFilenameSymbol", { | |
| 309 | + files: { | |
| 310 | + "/entry.js": /* js */ ` | |
| 311 | + const u = require("./café-utils.js"); | |
| 312 | + console.log(u.hi); | |
| 313 | + `, | |
| 314 | + "/café-utils.js": /* js */ ` | |
| 315 | + module.exports = { hi: 1 }; | |
| 316 | + `, | |
| 317 | + }, | |
| 318 | + target: "bun", | |
| 319 | + onAfterBundle(api) { | |
| 320 | + api.expectFile("/out.js").toContain("require_caf_utils"); | |
| 321 | + api.expectFile("/out.js").not.toContain("require_caf__utils"); | |
| 322 | + }, | |
| 323 | + run: { stdout: "1" }, | |
| 324 | + }); | |
| 305 | 325 | }); |
136
test/cli/install/bun-add.test.ts
@@ -2323,6 +2323,142 @@ it("should add local tarball dependency", async () => { | ||
| 2323 | 2323 | await access(join(package_dir, "bun.lockb"))); |
| 2324 | 2324 | }); |
| 2325 | 2325 | |
| 2326 | +it("should not add duplicate package.json entries when installing the same local folder twice (#30933)", async () => { | |
| 2327 | + setHandler(dummyRegistry([])); | |
| 2328 | + await writeFile( | |
| 2329 | + join(add_dir, "package.json"), | |
| 2330 | + JSON.stringify({ | |
| 2331 | + name: "myproject", | |
| 2332 | + version: "1.0.0", | |
| 2333 | + bin: { myproject: "./index.js" }, | |
| 2334 | + }), | |
| 2335 | + ); | |
| 2336 | + await writeFile(join(add_dir, "index.js"), 'console.log("hi")'); | |
| 2337 | + | |
| 2338 | + await writeFile( | |
| 2339 | + join(package_dir, "package.json"), | |
| 2340 | + JSON.stringify({ | |
| 2341 | + name: "host", | |
| 2342 | + version: "0.0.1", | |
| 2343 | + }), | |
| 2344 | + ); | |
| 2345 | + | |
| 2346 | + const local_path = resolve(add_dir); | |
| 2347 | + const stored_path = local_path.replace(/\\/g, "/"); | |
| 2348 | + | |
| 2349 | + { | |
| 2350 | + const { stdout, stderr, exited } = spawn({ | |
| 2351 | + cmd: [bunExe(), "add", local_path], | |
| 2352 | + cwd: package_dir, | |
| 2353 | + stdout: "pipe", | |
| 2354 | + stdin: "pipe", | |
| 2355 | + stderr: "pipe", | |
| 2356 | + env, | |
| 2357 | + }); | |
| 2358 | + const err = await stderr.text(); | |
| 2359 | + expect(err).not.toContain("error:"); | |
| 2360 | + expect(err).toContain("Saved lockfile"); | |
| 2361 | + const out = await stdout.text(); | |
| 2362 | + expect(out).toContain("installed myproject@"); | |
| 2363 | + expect(await exited).toBe(0); | |
| 2364 | + } | |
| 2365 | + | |
| 2366 | + { | |
| 2367 | + const { stdout, stderr, exited } = spawn({ | |
| 2368 | + cmd: [bunExe(), "add", local_path], | |
| 2369 | + cwd: package_dir, | |
| 2370 | + stdout: "pipe", | |
| 2371 | + stdin: "pipe", | |
| 2372 | + stderr: "pipe", | |
| 2373 | + env, | |
| 2374 | + }); | |
| 2375 | + const err = await stderr.text(); | |
| 2376 | + expect(err).not.toContain("error:"); | |
| 2377 | + expect(err).toContain("Saved lockfile"); | |
| 2378 | + const out = await stdout.text(); | |
| 2379 | + expect(out).toContain("installed myproject@"); | |
| 2380 | + expect(await exited).toBe(0); | |
| 2381 | + } | |
| 2382 | + | |
| 2383 | + const raw = await file(join(package_dir, "package.json")).text(); | |
| 2384 | + expect(raw.match(/"myproject"\s*:/g) ?? []).toHaveLength(1); | |
| 2385 | + expect(JSON.parse(raw)).toStrictEqual({ | |
| 2386 | + name: "host", | |
| 2387 | + version: "0.0.1", | |
| 2388 | + dependencies: { | |
| 2389 | + myproject: stored_path, | |
| 2390 | + }, | |
| 2391 | + }); | |
| 2392 | +}); | |
| 2393 | + | |
| 2394 | +it("should not add duplicate package.json entries when installing the same tarball URL twice (#30499)", async () => { | |
| 2395 | + using server = Bun.serve({ | |
| 2396 | + port: 0, | |
| 2397 | + fetch() { | |
| 2398 | + return new Response(Bun.file(join(__dirname, "baz-0.0.3.tgz"))); | |
| 2399 | + }, | |
| 2400 | + }); | |
| 2401 | + const tarball_url = `${server.url.href.replace(/\/+$/, "")}/baz-0.0.3.tgz`; | |
| 2402 | + setHandler(dummyRegistry([])); | |
| 2403 | + await writeFile( | |
| 2404 | + join(package_dir, "package.json"), | |
| 2405 | + JSON.stringify({ | |
| 2406 | + name: "foo", | |
| 2407 | + version: "0.0.1", | |
| 2408 | + }), | |
| 2409 | + ); | |
| 2410 | + | |
| 2411 | + { | |
| 2412 | + const { stdout, stderr, exited } = spawn({ | |
| 2413 | + cmd: [bunExe(), "add", tarball_url], | |
| 2414 | + cwd: package_dir, | |
| 2415 | + stdout: "pipe", | |
| 2416 | + stdin: "pipe", | |
| 2417 | + stderr: "pipe", | |
| 2418 | + env, | |
| 2419 | + }); | |
| 2420 | + const err = await stderr.text(); | |
| 2421 | + expect(err).toContain("Saved lockfile"); | |
| 2422 | + const out = await stdout.text(); | |
| 2423 | + expect(out).toContain("installed baz@"); | |
| 2424 | + expect(await exited).toBe(0); | |
| 2425 | + } | |
| 2426 | + expect(await file(join(package_dir, "package.json")).json()).toStrictEqual({ | |
| 2427 | + name: "foo", | |
| 2428 | + version: "0.0.1", | |
| 2429 | + dependencies: { | |
| 2430 | + baz: tarball_url, | |
| 2431 | + }, | |
| 2432 | + }); | |
| 2433 | + | |
| 2434 | + { | |
| 2435 | + const { stdout, stderr, exited } = spawn({ | |
| 2436 | + cmd: [bunExe(), "add", tarball_url], | |
| 2437 | + cwd: package_dir, | |
| 2438 | + stdout: "pipe", | |
| 2439 | + stdin: "pipe", | |
| 2440 | + stderr: "pipe", | |
| 2441 | + env, | |
| 2442 | + }); | |
| 2443 | + const err = await stderr.text(); | |
| 2444 | + expect(err).not.toContain("error:"); | |
| 2445 | + expect(err).toContain("Saved lockfile"); | |
| 2446 | + const out = await stdout.text(); | |
| 2447 | + expect(out).toContain("installed baz@"); | |
| 2448 | + expect(await exited).toBe(0); | |
| 2449 | + } | |
| 2450 | + | |
| 2451 | + const raw = await file(join(package_dir, "package.json")).text(); | |
| 2452 | + expect(raw.match(/"baz"\s*:/g) ?? []).toHaveLength(1); | |
| 2453 | + expect(JSON.parse(raw)).toStrictEqual({ | |
| 2454 | + name: "foo", | |
| 2455 | + version: "0.0.1", | |
| 2456 | + dependencies: { | |
| 2457 | + baz: tarball_url, | |
| 2458 | + }, | |
| 2459 | + }); | |
| 2460 | +}); | |
| 2461 | + | |
| 2326 | 2462 | it("should add multiple dependencies specified on command line", async () => { |
| 2327 | 2463 | expect(check_npm_auth_type.check).toBe(true); |
| 2328 | 2464 | using server = Bun.serve({ |
65
test/cli/install/bun-lockb.test.ts
@@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { file, spawn, write } from "bun"; |
| 2 | 2 | import { afterAll, beforeAll, expect, it } from "bun:test"; |
| 3 | -import { copyFile, exists, open, writeFile } from "fs/promises"; | |
| 4 | -import { bunExe, bunEnv as env, isWindows, runBunInstall, VerdaccioRegistry } from "harness"; | |
| 3 | +import { copyFile, exists, open, rm, writeFile } from "fs/promises"; | |
| 4 | +import { bunExe, bunEnv as env, isWindows, runBunInstall, stderrForInstall, VerdaccioRegistry } from "harness"; | |
| 5 | 5 | import { join } from "path"; |
| 6 | 6 | |
| 7 | 7 | const registry = new VerdaccioRegistry(); |
@@ -114,3 +114,64 @@ it("should continue using a binary lockfile if it exists", async () => { | ||
| 114 | 114 | const thirdLockfile = await file(join(packageDir, "bun.lockb")).text(); |
| 115 | 115 | expect(thirdLockfile).not.toBe(secondLockfile); |
| 116 | 116 | }); |
| 117 | + | |
| 118 | +it("recovers from a corrupted binary lockfile instead of panicking", async () => { | |
| 119 | + const { packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false } }); | |
| 120 | + | |
| 121 | + await write( | |
| 122 | + packageJson, | |
| 123 | + JSON.stringify({ | |
| 124 | + name: "corrupt-lockb", | |
| 125 | + version: "1.0.0", | |
| 126 | + dependencies: { | |
| 127 | + "no-deps": "1.0.0", | |
| 128 | + "a-dep": "1.0.1", | |
| 129 | + }, | |
| 130 | + }), | |
| 131 | + ); | |
| 132 | + | |
| 133 | + // Generate a valid bun.lockb against the local registry. | |
| 134 | + await runBunInstall(env, packageDir); | |
| 135 | + const lockbPath = join(packageDir, "bun.lockb"); | |
| 136 | + expect(await exists(lockbPath)).toBe(true); | |
| 137 | + | |
| 138 | + // Corrupt `meta[1].id` to an out-of-range value. Packages are stored | |
| 139 | + // SoA; the `meta` column sits after name (8), name_hash (8), resolution | |
| 140 | + // (72 for format v3, 64 for v2), dependencies (8) and resolutions (8) | |
| 141 | + // per package, and `id` is at +8 within each 88-byte Meta. | |
| 142 | + const lockb = Buffer.from(await file(lockbPath).arrayBuffer()); | |
| 143 | + const fmt = lockb.readUInt32LE(42); | |
| 144 | + const N = Number(lockb.readBigUInt64LE(86)); | |
| 145 | + const begin = Number(lockb.readBigUInt64LE(110)); | |
| 146 | + const resolutionSize = fmt === 2 ? 64 : 72; | |
| 147 | + const metaStart = begin + N * (8 + 8 + resolutionSize + 8 + 8); | |
| 148 | + expect(N).toBeGreaterThan(1); | |
| 149 | + // Sanity: in a well-formed lockfile meta[i].id == i. | |
| 150 | + expect(lockb.readUInt32LE(metaStart + 0 * 88 + 8)).toBe(0); | |
| 151 | + expect(lockb.readUInt32LE(metaStart + 1 * 88 + 8)).toBe(1); | |
| 152 | + lockb.writeUInt32LE(0x7fffffff, metaStart + 1 * 88 + 8); | |
| 153 | + await write(lockbPath, lockb); | |
| 154 | + | |
| 155 | + await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); | |
| 156 | + | |
| 157 | + const { stdout, stderr, exited } = spawn({ | |
| 158 | + cmd: [bunExe(), "install", "--no-progress"], | |
| 159 | + cwd: packageDir, | |
| 160 | + stdout: "pipe", | |
| 161 | + stderr: "pipe", | |
| 162 | + env, | |
| 163 | + }); | |
| 164 | + const [out, rawErr, code] = await Promise.all([stdout.text(), stderr.text(), exited]); | |
| 165 | + const err = stderrForInstall(rawErr); | |
| 166 | + | |
| 167 | + // The garbage `meta.id` deserialized from the corrupt lockfile used to | |
| 168 | + // panic_bounds_check in Package::clone. Released Bun tolerates it: it | |
| 169 | + // re-resolves and completes the install. The fix matches that. | |
| 170 | + expect(err).toContain("Ignoring lockfile"); | |
| 171 | + expect(err).not.toContain("error:"); | |
| 172 | + expect(out).toContain("[email protected]"); | |
| 173 | + expect(out).toContain("[email protected]"); | |
| 174 | + expect(code).toBe(0); | |
| 175 | + expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBe(true); | |
| 176 | + expect(await exists(join(packageDir, "node_modules", "a-dep"))).toBe(true); | |
| 177 | +}); | |
56
test/js/bun/resolve/resolve-autoinstall-log-dangling.test.ts
@@ -0,0 +1,56 @@ | ||
| 1 | +import { expect, test } from "bun:test"; | |
| 2 | +import { bunEnv, bunExe, tempDir } from "harness"; | |
| 3 | + | |
| 4 | +// When the runtime resolver's auto-install path lazily creates the | |
| 5 | +// PackageManager, it snapshots `resolver.log` into `pm.log`. If the | |
| 6 | +// PackageManager is created while the resolver log is pointed at a | |
| 7 | +// stack-local `Log` inside `resolveMaybeNeedsTrailingSlash`, `pm.log` must be | |
| 8 | +// restored to the VM log before returning, otherwise the next resolve at a | |
| 9 | +// different stack depth dereferences dead stack memory when the HTTP task | |
| 10 | +// fails and calls `manager.log.add_error_fmt(...)`. | |
| 11 | +// | |
| 12 | +// The Zig original already swaps and restores `linker.log` and | |
| 13 | +// `package_manager.log`; this test guards against regression. | |
| 14 | +test("repeated failing auto-install resolves at varying stack depth don't read a dangling pm.log", async () => { | |
| 15 | + using dir = tempDir("resolve-autoinstall-log", {}); | |
| 16 | + | |
| 17 | + const src = ` | |
| 18 | + const realm = new ShadowRealm(); | |
| 19 | + const variants = [ | |
| 20 | + () => realm.importValue("pkg-not-found-a", "x"), | |
| 21 | + () => (() => realm.importValue("pkg-not-found-b", "x"))(), | |
| 22 | + () => (() => (() => realm.importValue("pkg-not-found-c", "x"))())(), | |
| 23 | + () => { try { return realm.importValue("pkg-not-found-d", "x"); } finally {} }, | |
| 24 | + () => [realm.importValue("pkg-not-found-e", "x")][0], | |
| 25 | + () => import("pkg-not-found-f"), | |
| 26 | + () => (async () => realm.importValue("pkg-not-found-g", "x"))(), | |
| 27 | + ]; | |
| 28 | + for (let i = 0; i < 100; i++) { | |
| 29 | + for (const v of variants) { | |
| 30 | + try { | |
| 31 | + const p = v(); | |
| 32 | + if (p && p.catch) p.catch(() => {}); | |
| 33 | + } catch {} | |
| 34 | + } | |
| 35 | + } | |
| 36 | + Bun.gc(true); | |
| 37 | + console.log("ok"); | |
| 38 | + `; | |
| 39 | + | |
| 40 | + await using proc = Bun.spawn({ | |
| 41 | + cmd: [bunExe(), "--install=fallback", "-e", src], | |
| 42 | + cwd: String(dir), | |
| 43 | + env: { | |
| 44 | + ...bunEnv, | |
| 45 | + BUN_CONFIG_REGISTRY: "http://127.0.0.1:1", | |
| 46 | + }, | |
| 47 | + stdout: "pipe", | |
| 48 | + stderr: "pipe", | |
| 49 | + }); | |
| 50 | + | |
| 51 | + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | |
| 52 | + | |
| 53 | + expect(stderr).toBe(""); | |
| 54 | + expect(stdout.trim()).toBe("ok"); | |
| 55 | + expect(exitCode).toBe(0); | |
| 56 | +}); | |
27
test/js/bun/shell/brace.test.ts
@@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { $ } from "bun"; |
| 2 | 2 | import { describe, expect, test } from "bun:test"; |
| 3 | +import { tempDir } from "harness"; | |
| 3 | 4 | |
| 4 | 5 | describe("$.braces", () => { |
| 5 | 6 | test("no-op", () => { |
@@ -77,3 +78,29 @@ describe("$.braces", () => { | ||
| 77 | 78 | expect(result).toEqual(["lol 😂", "lol 🫵", "lol 🤣"]); |
| 78 | 79 | }); |
| 79 | 80 | }); |
| 81 | + | |
| 82 | +describe("brace + glob composition", () => { | |
| 83 | + test("src/*.{ts,tsx} globs after brace expansion", async () => { | |
| 84 | + using dir = tempDir("shell-brace-glob", { | |
| 85 | + "src/app.ts": "", | |
| 86 | + "src/util.tsx": "", | |
| 87 | + }); | |
| 88 | + const out = (await $`echo src/*.{ts,tsx}`.cwd(String(dir)).text()).trim().replaceAll("\\", "/"); | |
| 89 | + const words = out.split(" "); | |
| 90 | + expect(words).toContain("src/app.ts"); | |
| 91 | + expect(words).toContain("src/util.tsx"); | |
| 92 | + expect(words).toContain("src/*.ts"); | |
| 93 | + expect(words).toContain("src/*.tsx"); | |
| 94 | + }); | |
| 95 | + | |
| 96 | + test("{src,lib}/*.ts composes a brace prefix with a glob", async () => { | |
| 97 | + using dir = tempDir("shell-brace-glob2", { | |
| 98 | + "src/a.ts": "", | |
| 99 | + "lib/b.ts": "", | |
| 100 | + }); | |
| 101 | + const out = (await $`echo {src,lib}/*.ts`.cwd(String(dir)).text()).trim().replaceAll("\\", "/"); | |
| 102 | + const words = out.split(" "); | |
| 103 | + expect(words).toContain("src/a.ts"); | |
| 104 | + expect(words).toContain("lib/b.ts"); | |
| 105 | + }); | |
| 106 | +}); | |
31
test/js/bun/spawn/spawn-kill-signal.test.ts
@@ -50,4 +50,35 @@ describe("subprocess.kill", () => { | ||
| 50 | 50 | }); |
| 51 | 51 | } |
| 52 | 52 | }); |
| 53 | + | |
| 54 | + test("invalid signal name lists the valid signal names", async () => { | |
| 55 | + const proc = Bun.spawn({ | |
| 56 | + cmd: [shellExe(), "-c", "sleep 1000"], | |
| 57 | + stdio: ["inherit", "inherit", "inherit"], | |
| 58 | + }); | |
| 59 | + | |
| 60 | + let err: any; | |
| 61 | + try { | |
| 62 | + proc.kill("SIGGOD"); | |
| 63 | + } catch (e) { | |
| 64 | + err = e; | |
| 65 | + } | |
| 66 | + | |
| 67 | + expect(err).toBeInstanceOf(TypeError); | |
| 68 | + expect(err.code).toBe("ERR_INVALID_ARG_TYPE"); | |
| 69 | + // The message must enumerate the real signal names, not a static | |
| 70 | + // "the SignalCode names" placeholder (regressed in the Rust port; | |
| 71 | + // Zig already builds the list at comptime). | |
| 72 | + expect(err.message).toContain("'SIGHUP'"); | |
| 73 | + expect(err.message).toContain("'SIGTERM'"); | |
| 74 | + expect(err.message).toContain("'SIGKILL'"); | |
| 75 | + expect(err.message).toContain("or 'SIGSYS'"); | |
| 76 | + expect(err.message).not.toContain("the SignalCode names"); | |
| 77 | + | |
| 78 | + const { promise, resolve, reject } = Promise.withResolvers(); | |
| 79 | + proc.exited.then(resolve, reject); | |
| 80 | + proc.kill(); | |
| 81 | + await promise; | |
| 82 | + expect(proc.signalCode).toBe("SIGTERM"); | |
| 83 | + }); | |
| 53 | 84 | }); |
10
test/js/bun/util/filesink.test.ts
@@ -268,3 +268,13 @@ it.skipIf(!isPosix)("does not leak native FileSink when a pending write fails (E | ||
| 268 | 268 | // more than that indicates a native leak. |
| 269 | 269 | expect(fileSinkInternals.liveCount()).toBeLessThanOrEqual(baseline + 1); |
| 270 | 270 | }); |
| 271 | + | |
| 272 | +it("start() without path/fd on an already-open writer does not crash", async () => { | |
| 273 | + const path = join(tmpdirSync(), "filesink-restart.txt"); | |
| 274 | + const writer = Bun.file(path).writer(); | |
| 275 | + expect(() => writer.start({})).not.toThrow(); | |
| 276 | + expect(() => writer.start({ highWaterMark: 1024 })).not.toThrow(); | |
| 277 | + writer.write("hello"); | |
| 278 | + await writer.end(); | |
| 279 | + expect(await Bun.file(path).text()).toBe("hello"); | |
| 280 | +}); | |
30
test/js/node/events/event-emitter.test.ts
@@ -65,6 +65,36 @@ describe("node:events", () => { | ||
| 65 | 65 | await promise; |
| 66 | 66 | expect(emitter.listenerCount("hey")).toBe(0); |
| 67 | 67 | }); |
| 68 | + | |
| 69 | + // `events.once()` is an `async function` in Node: a bad `options`, a bad | |
| 70 | + // `options.signal`, or an already-aborted signal must produce a *rejected | |
| 71 | + // promise*, never a synchronous throw. | |
| 72 | + test("once is an async function", () => { | |
| 73 | + expect(EventEmitter.once.constructor.name).toBe("AsyncFunction"); | |
| 74 | + }); | |
| 75 | + | |
| 76 | + test("once with already-aborted signal rejects (not a synchronous throw)", async () => { | |
| 77 | + const ee = new EventEmitter(); | |
| 78 | + const p = EventEmitter.once(ee, "foo", { signal: AbortSignal.abort() }); | |
| 79 | + expect(p).toBeInstanceOf(Promise); | |
| 80 | + await expect(p).rejects.toMatchObject({ name: "AbortError", code: "ABORT_ERR" }); | |
| 81 | + }); | |
| 82 | + | |
| 83 | + test("once with invalid options.signal rejects (not a synchronous throw)", async () => { | |
| 84 | + for (const signal of [1, {}, "hi", null, false]) { | |
| 85 | + const ee = new EventEmitter(); | |
| 86 | + const p = EventEmitter.once(ee, "foo", { signal } as any); | |
| 87 | + expect(p).toBeInstanceOf(Promise); | |
| 88 | + await expect(p).rejects.toMatchObject({ code: "ERR_INVALID_ARG_TYPE" }); | |
| 89 | + } | |
| 90 | + }); | |
| 91 | + | |
| 92 | + test("once with non-object options rejects (not a synchronous throw)", async () => { | |
| 93 | + const ee = new EventEmitter(); | |
| 94 | + const p = EventEmitter.once(ee, "foo", "hi" as any); | |
| 95 | + expect(p).toBeInstanceOf(Promise); | |
| 96 | + await expect(p).rejects.toMatchObject({ code: "ERR_INVALID_ARG_TYPE" }); | |
| 97 | + }); | |
| 68 | 98 | }); |
| 69 | 99 | |
| 70 | 100 | describe("EventEmitter", () => { |
31
test/js/node/fs/fs-readfile-fifo-fixture.js
@@ -0,0 +1,31 @@ | ||
| 1 | +// Spawned by fs.test.ts. Reads a FIFO whose content (>256 KB) far exceeds the | |
| 2 | +// 0-byte size `fstat` reports for a pipe. The "stat size is wrong" grow path in | |
| 3 | +// readFileSync reset buf.items.len = total before each reserve so growth is | |
| 4 | +// incremental. Prints a parseable line so the parent can assert. | |
| 5 | +const fs = require("fs"); | |
| 6 | +const cp = require("child_process"); | |
| 7 | +const path = require("path"); | |
| 8 | + | |
| 9 | +const dir = process.argv[2]; | |
| 10 | +const fifo = path.join(dir, "thefifo"); | |
| 11 | +try { | |
| 12 | + fs.unlinkSync(fifo); | |
| 13 | +} catch {} | |
| 14 | +cp.execFileSync("mkfifo", [fifo]); | |
| 15 | +if (!fs.statSync(fifo).isFIFO()) throw new Error(`not a FIFO: ${fifo}`); | |
| 16 | + | |
| 17 | +const SIZE = 400 * 1024; | |
| 18 | +cp.spawn( | |
| 19 | + process.execPath, | |
| 20 | + [ | |
| 21 | + "-e", | |
| 22 | + `const fs=require("fs");const fd=fs.openSync(process.argv[1],"w");` + | |
| 23 | + `const b=Buffer.alloc(${SIZE},0x61);let o=0;` + | |
| 24 | + `while(o<b.length){o+=fs.writeSync(fd,b,o,Math.min(16384,b.length-o))}fs.closeSync(fd);`, | |
| 25 | + fifo, | |
| 26 | + ], | |
| 27 | + { stdio: "inherit" }, | |
| 28 | +); | |
| 29 | + | |
| 30 | +const data = fs.readFileSync(fifo); | |
| 31 | +process.stdout.write(`len=${data.length} allA=${data.every(x => x === 0x61)}`); | |
23
test/js/node/fs/fs.test.ts
@@ -3968,3 +3968,26 @@ describe("synchronous I/O string flags", () => { | ||
| 3968 | 3968 | expect(buf.toString("utf8", 0, bytesRead)).toBe("hello"); |
| 3969 | 3969 | }); |
| 3970 | 3970 | }); |
| 3971 | + | |
| 3972 | +// POSIX-only: readFileSync on a FIFO whose fstat size is 0 but actual | |
| 3973 | +// content >256 KB. Zig resets buf.items.len = total before each reserve, | |
| 3974 | +// so growth is incremental; the test guard is that it returns promptly | |
| 3975 | +// instead of ballooning RSS. | |
| 3976 | +if (isPosix) { | |
| 3977 | + describe("readFileSync on a FIFO larger than the stat size", () => { | |
| 3978 | + it("does not balloon the read buffer", async () => { | |
| 3979 | + using dir = tempDir("fs-readfile-fifo", {}); | |
| 3980 | + const fixture = join(import.meta.path, "../fs-readfile-fifo-fixture.js"); | |
| 3981 | + await using proc = Bun.spawn({ | |
| 3982 | + cmd: [bunExe(), fixture, String(dir)], | |
| 3983 | + env: bunEnv, | |
| 3984 | + stdout: "pipe", | |
| 3985 | + stderr: "pipe", | |
| 3986 | + }); | |
| 3987 | + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | |
| 3988 | + expect(stderr).toBe(""); | |
| 3989 | + expect(stdout.trim()).toBe("len=409600 allA=true"); | |
| 3990 | + expect(exitCode).toBe(0); | |
| 3991 | + }); | |
| 3992 | + }); | |
| 3993 | +} | |
37
test/regression/issue/30963.test.ts
@@ -0,0 +1,37 @@ | ||
| 1 | +// https://github.com/oven-sh/bun/issues/30963 | |
| 2 | +// | |
| 3 | +// Parsing `[{static{}` (a `static { ... }` block inside an object literal | |
| 4 | +// instead of a class body) tripped a debug_assert in the object-literal | |
| 5 | +// parser: the class-static-block branch in `parseProperty.zig` fired on | |
| 6 | +// identifier + `{` alone, without checking that the enclosing context was | |
| 7 | +// actually a class, and returned a property with neither key nor | |
| 8 | +// value, violating the assertion in the caller. | |
| 9 | +// | |
| 10 | +// Run the parse in a subprocess so a reintroduction (debug-build abort) | |
| 11 | +// doesn't tear down the in-process test runner. | |
| 12 | + | |
| 13 | +import { expect, test } from "bun:test"; | |
| 14 | +import { bunEnv, bunExe } from "harness"; | |
| 15 | + | |
| 16 | +test.concurrent.each(["[{static{}", "({static{}})", "({static{};})", "({static{},})"])( | |
| 17 | + "`static { ... }` in an object-literal position (%s) is a syntax error, not a parser crash", | |
| 18 | + async source => { | |
| 19 | + await using proc = Bun.spawn({ | |
| 20 | + cmd: [bunExe(), "-e", source], | |
| 21 | + env: bunEnv, | |
| 22 | + stdin: "ignore", | |
| 23 | + stdout: "pipe", | |
| 24 | + stderr: "pipe", | |
| 25 | + timeout: 10_000, | |
| 26 | + }); | |
| 27 | + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | |
| 28 | + | |
| 29 | + // Before the fix, the debug build aborted inside the | |
| 30 | + // parser. After the fix the parser emits the | |
| 31 | + // normal user-facing syntax error. | |
| 32 | + expect(stderr).toContain('Expected "}" but found "{"'); | |
| 33 | + expect(proc.signalCode).toBeNull(); | |
| 34 | + expect(stdout).toBe(""); | |
| 35 | + expect(exitCode).not.toBe(0); | |
| 36 | + }, | |
| 37 | +); | |
43
test/regression/issue/jsx-template-string-crash.test.ts
@@ -17,14 +17,49 @@ test("JSX lexer should not crash with slice bounds issues", async () => { | ||
| 17 | 17 | expect(exitCode).toBe(1); |
| 18 | 18 | expect(normalizeBunSnapshot(stderr.toString().replace(/(Bun v.*)$/gm, ""))).toMatchInlineSnapshot(` |
| 19 | 19 | "1 | export function x(){return<div a=\`\`/>} |
| 20 | - ^ | |
| 21 | - error: Expected "{" but found "\`" | |
| 20 | + ^ | |
| 21 | + error: Expecting Unicode escape sequence \\uXXXX | |
| 22 | 22 | at <cwd>/[eval]:1:34 |
| 23 | 23 | |
| 24 | 24 | 1 | export function x(){return<div a=\`\`/>} |
| 25 | 25 | ^ |
| 26 | - error: Unexpected > | |
| 27 | - at <cwd>/[eval]:1:37" | |
| 26 | + error: Expected "{" but found "\`" | |
| 27 | + at <cwd>/[eval]:1:34 | |
| 28 | + | |
| 29 | + 1 | export function x(){return<div a=\`\`/>} | |
| 30 | + ^ | |
| 31 | + error: Unterminated string literal | |
| 32 | + at <cwd>/[eval]:1:35" | |
| 28 | 33 | `); |
| 29 | 34 | expect(normalizeBunSnapshot(stdout.toString())).toMatchInlineSnapshot(`""`); |
| 30 | 35 | }); |
| 36 | + | |
| 37 | +test.concurrent("#30959 JSX attribute with invalid '(' value parses cleanly in debug builds", async () => { | |
| 38 | + // Previously, parsing `<r L=((` in a debug build panicked with | |
| 39 | + // `Scope location must be greater than previous 6 must be greater than 6` | |
| 40 | + // in push_scope_for_parse_pass. `nextInsideJSXElement`'s syntax-error | |
| 41 | + // branch set `end = current` on the first `(` without stepping past it, | |
| 42 | + // causing re-dispatch on the same byte and duplicate scope offsets. | |
| 43 | + await using proc = Bun.spawn({ | |
| 44 | + cmd: [bunExe(), "-e", "export function x(){return<r L=((}"], | |
| 45 | + env: bunEnv, | |
| 46 | + stderr: "pipe", | |
| 47 | + stdout: "pipe", | |
| 48 | + }); | |
| 49 | + | |
| 50 | + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | |
| 51 | + | |
| 52 | + expect(normalizeBunSnapshot(stderr.replace(/(Bun v.*)$/gm, ""))).toMatchInlineSnapshot(` | |
| 53 | + "1 | export function x(){return<r L=((} | |
| 54 | + ^ | |
| 55 | + error: Expected "{" but found "(" | |
| 56 | + at <cwd>/[eval]:1:32 | |
| 57 | + | |
| 58 | + 1 | export function x(){return<r L=((} | |
| 59 | + ^ | |
| 60 | + error: Unexpected } | |
| 61 | + at <cwd>/[eval]:1:34" | |
| 62 | + `); | |
| 63 | + expect(stdout).toBe(""); | |
| 64 | + expect(exitCode).toBe(1); | |
| 65 | +}); | |