diff --git a/.gitignore b/.gitignore index 7b7898b3cf..8491d87032 100644 --- a/.gitignore +++ b/.gitignore @@ -141,6 +141,14 @@ x64 yarn.lock zig-cache zig-out +test/node.js/upstream +.zig-cache +scripts/env.local +*.generated.ts +src/bake/generated.ts +test/cli/install/registry/packages/publish-pkg-* +test/cli/install/registry/packages/@secret/publish-pkg-8 +test/js/third_party/prisma/prisma/sqlite/dev.db-journal # Dependencies /vendor diff --git a/src/cli/bunx_command.zig b/src/cli/bunx_command.zig index 1f6adc026b..7f3113ec8f 100644 --- a/src/cli/bunx_command.zig +++ b/src/cli/bunx_command.zig @@ -323,6 +323,9 @@ pub const BunxCommand = struct { root_dir_info.abs_path, ctx.debug.run_in_bun, ); + this_bundler.env.map.put("npm_command", "exec") catch unreachable; + this_bundler.env.map.put("npm_lifecycle_event", "bunx") catch unreachable; + this_bundler.env.map.put("npm_lifecycle_script", package_name) catch unreachable; const ignore_cwd = this_bundler.env.get("BUN_WHICH_IGNORE_CWD") orelse ""; diff --git a/src/cli/pack_command.zig b/src/cli/pack_command.zig index 6a3d7adbe0..73fdd87118 100644 --- a/src/cli/pack_command.zig +++ b/src/cli/pack_command.zig @@ -1160,6 +1160,7 @@ pub const PackCommand = struct { }; const abs_workspace_path: string = strings.withoutTrailingSlash(strings.withoutSuffixComptime(abs_package_json_path, "package.json")); + try manager.env.map.put("npm_command", "pack"); const postpack_script, const publish_script: ?[]const u8, const postpublish_script: ?[]const u8 = post_scripts: { // --ignore-scripts diff --git a/src/cli/publish_command.zig b/src/cli/publish_command.zig index 69c3e6d12f..466f7ef488 100644 --- a/src/cli/publish_command.zig +++ b/src/cli/publish_command.zig @@ -441,6 +441,8 @@ pub const PublishCommand = struct { if (manager.options.do.run_scripts) { const abs_workspace_path: string = strings.withoutTrailingSlash(strings.withoutSuffixComptime(manager.original_package_json_path, "package.json")); + try context.script_env.map.put("npm_command", "publish"); + if (context.publish_script) |publish_script| { _ = Run.runPackageScriptForeground( context.command_ctx, diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 3267b38de8..51fea5bed3 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -259,6 +259,8 @@ pub const RunCommand = struct { use_system_shell: bool, ) !void { const shell_bin = findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell; + env.map.put("npm_lifecycle_event", name) catch unreachable; + env.map.put("npm_lifecycle_script", original_script) catch unreachable; var copy_script_capacity: usize = original_script.len; for (passthrough) |part| copy_script_capacity += 1 + part.len; @@ -872,6 +874,14 @@ pub const RunCommand = struct { this_bundler.env.map.put(NpmArgs.package_version, package_json.version) catch unreachable; } } + + if (package_json.config) |config| { + try this_bundler.env.map.ensureUnusedCapacity(config.count()); + for (config.keys(), config.values()) |k, v| { + const key = try bun.strings.concat(bun.default_allocator, &.{ "npm_package_config_", k }); + this_bundler.env.map.putAssumeCapacity(key, v); + } + } } return root_dir_info; @@ -1384,7 +1394,7 @@ pub const RunCommand = struct { var this_bundler: bundler.Bundler = undefined; const root_dir_info = try configureEnvForRun(ctx, &this_bundler, null, log_errors, false); try configurePathForRun(ctx, root_dir_info, &this_bundler, &ORIGINAL_PATH, root_dir_info.abs_path, force_using_bun); - this_bundler.env.map.put("npm_lifecycle_event", script_name_to_search) catch unreachable; + this_bundler.env.map.put("npm_command", "run-script") catch unreachable; if (script_name_to_search.len == 0) { // naked "bun run" @@ -1421,17 +1431,19 @@ pub const RunCommand = struct { ); } - try runPackageScriptForeground( - ctx, - ctx.allocator, - script_content, - script_name_to_search, - this_bundler.fs.top_level_dir, - this_bundler.env, - passthrough, - ctx.debug.silent, - ctx.debug.use_system_shell, - ); + { + try runPackageScriptForeground( + ctx, + ctx.allocator, + script_content, + script_name_to_search, + this_bundler.fs.top_level_dir, + this_bundler.env, + passthrough, + ctx.debug.silent, + ctx.debug.use_system_shell, + ); + } temp_script_buffer[0.."post".len].* = "post".*; diff --git a/src/env_loader.zig b/src/env_loader.zig index 6c0c908796..77cc2aa7ec 100644 --- a/src/env_loader.zig +++ b/src/env_loader.zig @@ -1194,6 +1194,20 @@ pub const Map = struct { }); } + pub fn ensureUnusedCapacity(this: *Map, additional_count: usize) !void { + return this.map.ensureUnusedCapacity(additional_count); + } + + pub fn putAssumeCapacity(this: *Map, key: string, value: string) void { + if (Environment.isWindows and Environment.allow_assert) { + bun.assert(bun.strings.indexOfChar(key, '\x00') == null); + } + this.map.putAssumeCapacity(key, .{ + .value = value, + .conditional = false, + }); + } + pub inline fn putAllocKeyAndValue(this: *Map, allocator: std.mem.Allocator, key: string, value: string) !void { const gop = try this.map.getOrPut(key); gop.value_ptr.* = .{ diff --git a/src/js_ast.zig b/src/js_ast.zig index 2d752179ad..c7713a3dde 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -3533,6 +3533,39 @@ pub const Expr = struct { return obj.asProperty(name); } + pub fn asPropertyStringMap(expr: *const Expr, name: string, allocator: std.mem.Allocator) ?*bun.StringArrayHashMap(string) { + if (std.meta.activeTag(expr.data) != .e_object) return null; + const obj_ = expr.data.e_object; + if (@intFromPtr(obj_.properties.ptr) == 0) return null; + const query = obj_.asProperty(name) orelse return null; + if (query.expr.data != .e_object) return null; + + const obj = query.expr.data.e_object; + var count: usize = 0; + for (obj.properties.slice()) |prop| { + const key = prop.key.?.asString(allocator) orelse continue; + const value = prop.value.?.asString(allocator) orelse continue; + count += @as(usize, @intFromBool(key.len > 0 and value.len > 0)); + } + + if (count == 0) return null; + var map = bun.StringArrayHashMap(string).init(allocator); + map.ensureUnusedCapacity(count) catch return null; + + for (obj.properties.slice()) |prop| { + const key = prop.key.?.asString(allocator) orelse continue; + const value = prop.value.?.asString(allocator) orelse continue; + + if (!(key.len > 0 and value.len > 0)) continue; + + map.putAssumeCapacity(key, value); + } + + const ptr = allocator.create(bun.StringArrayHashMap(string)) catch unreachable; + ptr.* = map; + return ptr; + } + pub const ArrayIterator = struct { array: *const E.Array, index: u32, diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index fa93fed2e8..b801b6c11c 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -106,6 +106,7 @@ pub const PackageJSON = struct { hash: u32 = 0xDEADBEEF, scripts: ?*ScriptsMap = null, + config: ?*bun.StringArrayHashMap(string) = null, arch: Architecture = Architecture.all, os: OperatingSystem = OperatingSystem.all, @@ -1006,36 +1007,11 @@ pub const PackageJSON = struct { // used by `bun run` if (include_scripts) { - read_scripts: { - if (json.asProperty("scripts")) |scripts_prop| { - if (scripts_prop.expr.data == .e_object) { - const scripts_obj = scripts_prop.expr.data.e_object; - - var count: usize = 0; - for (scripts_obj.properties.slice()) |prop| { - const key = prop.key.?.asString(allocator) orelse continue; - const value = prop.value.?.asString(allocator) orelse continue; - - count += @as(usize, @intFromBool(key.len > 0 and value.len > 0)); - } - - if (count == 0) break :read_scripts; - var scripts = ScriptsMap.init(allocator); - scripts.ensureUnusedCapacity(count) catch break :read_scripts; - - for (scripts_obj.properties.slice()) |prop| { - const key = prop.key.?.asString(allocator) orelse continue; - const value = prop.value.?.asString(allocator) orelse continue; - - if (!(key.len > 0 and value.len > 0)) continue; - - scripts.putAssumeCapacity(key, value); - } - - package_json.scripts = allocator.create(ScriptsMap) catch unreachable; - package_json.scripts.?.* = scripts; - } - } + if (json.asPropertyStringMap("scripts", allocator)) |scripts| { + package_json.scripts = scripts; + } + if (json.asPropertyStringMap("config", allocator)) |config| { + package_json.config = config; } } diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 52f0e1d7f6..37b3d09563 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -1005,3 +1005,65 @@ test("unicode", async () => { const tarball = readTarball(join(packageDir, "pack-unicode-1.1.1.tgz")); expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/äöüščří.js" }]); }); + +test("$npm_command is accurate", async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "pack-command", + version: "1.1.1", + scripts: { + postpack: "echo $npm_command", + }, + }), + ); + const p = await pack(packageDir, bunEnv); + expect(p.out.split("\n")).toEqual([ + `bun pack ${Bun.version_with_sha}`, + ``, + `packed 94B package.json`, + ``, + `pack-command-1.1.1.tgz`, + ``, + `Total files: 1`, + expect.stringContaining(`Shasum: `), + expect.stringContaining(`Integrity: sha512-`), + `Unpacked size: 94B`, + expect.stringContaining(`Packed size: `), + ``, + `pack`, + ``, + ]); + expect(p.err).toEqual(`$ echo $npm_command\n`); +}); + +test("$npm_lifecycle_event is accurate", async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "pack-lifecycle", + version: "1.1.1", + scripts: { + postpack: "echo $npm_lifecycle_event", + }, + }), + ); + const p = await pack(packageDir, bunEnv); + expect(p.out.split("\n")).toEqual([ + `bun pack ${Bun.version_with_sha}`, + ``, + `packed 104B package.json`, + ``, + `pack-lifecycle-1.1.1.tgz`, + ``, + `Total files: 1`, + expect.stringContaining(`Shasum: `), + expect.stringContaining(`Integrity: sha512-`), + `Unpacked size: 104B`, + expect.stringContaining(`Packed size: `), + ``, + `postpack`, + ``, + ]); + expect(p.err).toEqual(`$ echo $npm_lifecycle_event\n`); +}); diff --git a/test/cli/install/bun-run.test.ts b/test/cli/install/bun-run.test.ts index ab3ca92428..dc1866ec4b 100644 --- a/test/cli/install/bun-run.test.ts +++ b/test/cli/install/bun-run.test.ts @@ -476,6 +476,75 @@ it("--ignore-dce-annotations ignores DCE annotations", () => { expect(stdout.toString()).toBe("Hello, world!\n"); }); +it("$npm_command is accurate", async () => { + await writeFile( + join(run_dir, "package.json"), + `{ + "scripts": { + "sample": "echo $npm_command", + }, + } + `, + ); + const p = spawn({ + cmd: [bunExe(), "run", "sample"], + cwd: run_dir, + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + expect(await p.exited).toBe(0); + expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_command\n`); + expect(await new Response(p.stdout).text()).toBe(`run-script\n`); +}); + +it("$npm_lifecycle_event is accurate", async () => { + await writeFile( + join(run_dir, "package.json"), + `{ + "scripts": { + "presample": "echo $npm_lifecycle_event", + "sample": "echo $npm_lifecycle_event", + "postsample": "echo $npm_lifecycle_event", + }, + } + `, + ); + const p = spawn({ + cmd: [bunExe(), "run", "sample"], + cwd: run_dir, + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + expect(await p.exited).toBe(0); + // prettier-ignore + expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_lifecycle_event\n$ echo $npm_lifecycle_event\n$ echo $npm_lifecycle_event\n`,); + expect(await new Response(p.stdout).text()).toBe(`presample\nsample\npostsample\n`); +}); + +it("$npm_package_config_* works", async () => { + await writeFile( + join(run_dir, "package.json"), + `{ + "config": { + "foo": "bar" + }, + "scripts": { + "sample": "echo $npm_package_config_foo", + }, + } + `, + ); + const p = spawn({ + cmd: [bunExe(), "run", "sample"], + cwd: run_dir, + stdio: ["ignore", "pipe", "pipe"], + env: bunEnv, + }); + expect(await p.exited).toBe(0); + expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_package_config_foo\n`); + expect(await new Response(p.stdout).text()).toBe(`bar\n`); +}); + it("should pass arguments correctly in scripts", async () => { const dir = tempDirWithFiles("test", { "package.json": JSON.stringify({ diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index f692c72215..7cf4d49d37 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -601,3 +601,60 @@ describe("relative tarballs", async () => { }); }); }); + +test("$npm_package_config_ works in root", async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["pkgs/*"], + config: { foo: "bar" }, + scripts: { sample: "echo $npm_package_config_foo $npm_package_config_qux" }, + }), + ); + await write( + join(packageDir, "pkgs", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + config: { qux: "tab" }, + scripts: { sample: "echo $npm_package_config_foo $npm_package_config_qux" }, + }), + ); + const p = Bun.spawn({ + cmd: [bunExe(), "run", "sample"], + cwd: packageDir, + stdio: ["ignore", "pipe", "pipe"], + env, + }); + expect(await p.exited).toBe(0); + expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_package_config_foo $npm_package_config_qux\n`); + expect(await new Response(p.stdout).text()).toBe(`bar\n`); +}); +test("$npm_package_config_ works in root in subpackage", async () => { + await write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["pkgs/*"], + config: { foo: "bar" }, + scripts: { sample: "echo $npm_package_config_foo $npm_package_config_qux" }, + }), + ); + await write( + join(packageDir, "pkgs", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + config: { qux: "tab" }, + scripts: { sample: "echo $npm_package_config_foo $npm_package_config_qux" }, + }), + ); + const p = Bun.spawn({ + cmd: [bunExe(), "run", "sample"], + cwd: join(packageDir, "pkgs", "pkg1"), + stdio: ["ignore", "pipe", "pipe"], + env, + }); + expect(await p.exited).toBe(0); + expect(await new Response(p.stderr).text()).toBe(`$ echo $npm_package_config_foo $npm_package_config_qux\n`); + expect(await new Response(p.stdout).text()).toBe(`tab\n`); +}); diff --git a/test/cli/install/registry/bun-install-registry.test.ts b/test/cli/install/registry/bun-install-registry.test.ts index 3baacc0d2f..6475dbfe64 100644 --- a/test/cli/install/registry/bun-install-registry.test.ts +++ b/test/cli/install/registry/bun-install-registry.test.ts @@ -12055,3 +12055,79 @@ registry = "http://localhost:${port}/" }); } }); + +it("$npm_command is accurate during publish", async () => { + await write( + packageJson, + JSON.stringify({ + name: "publish-pkg-10", + version: "1.0.0", + scripts: { + publish: "echo $npm_command", + }, + }), + ); + await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_command")); + await rm(join(import.meta.dir, "packages", "publish-pkg-10"), { recursive: true, force: true }); + let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); + expect(err).toBe(`$ echo $npm_command\n`); + expect(out.split("\n")).toEqual([ + `bun publish ${Bun.version_with_sha}`, + ``, + `packed 95B package.json`, + ``, + `Total files: 1`, + expect.stringContaining(`Shasum: `), + expect.stringContaining(`Integrity: sha512-`), + `Unpacked size: 95B`, + expect.stringContaining(`Packed size: `), + `Tag: simpletag`, + `Access: default`, + `Registry: http://localhost:${port}/`, + ``, + ` + publish-pkg-10@1.0.0`, + `publish`, + ``, + ]); + expect(exitCode).toBe(0); +}); + +it("$npm_lifecycle_event is accurate during publish", async () => { + await write( + packageJson, + `{ + "name": "publish-pkg-11", + "version": "1.0.0", + "scripts": { + "prepublish": "echo 1 $npm_lifecycle_event", + "publish": "echo 2 $npm_lifecycle_event", + "postpublish": "echo 3 $npm_lifecycle_event", + }, + } + `, + ); + await write(join(packageDir, "bunfig.toml"), await authBunfig("npm_lifecycle_event")); + await rm(join(import.meta.dir, "packages", "publish-pkg-11"), { recursive: true, force: true }); + let { out, err, exitCode } = await publish(env, packageDir, "--tag", "simpletag"); + expect(err).toBe(`$ echo 2 $npm_lifecycle_event\n$ echo 3 $npm_lifecycle_event\n`); + expect(out.split("\n")).toEqual([ + `bun publish ${Bun.version_with_sha}`, + ``, + `packed 256B package.json`, + ``, + `Total files: 1`, + expect.stringContaining(`Shasum: `), + expect.stringContaining(`Integrity: sha512-`), + `Unpacked size: 256B`, + expect.stringContaining(`Packed size: `), + `Tag: simpletag`, + `Access: default`, + `Registry: http://localhost:${port}/`, + ``, + ` + publish-pkg-11@1.0.0`, + `2 publish`, + `3 postpublish`, + ``, + ]); + expect(exitCode).toBe(0); +}); diff --git a/test/cli/run/env.test.ts b/test/cli/run/env.test.ts index c2f3439a2b..2bf13d3a9f 100644 --- a/test/cli/run/env.test.ts +++ b/test/cli/run/env.test.ts @@ -704,7 +704,7 @@ console.log(dynamic().NODE_ENV); test("NODE_ENV default is not propogated in bun run", () => { const getenv = process.platform !== "win32" - ? "env | grep NODE_ENV && exit 1 || true" + ? "env | grep -v npm_lifecycle_script | grep NODE_ENV && exit 1 || true" : "node -e 'if(process.env.NODE_ENV)throw(1)'"; const tmp = tempDirWithFiles("default-node-env", { "package.json": '{"scripts":{"show-env":' + JSON.stringify(getenv) + "}}",