Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shell: make sure proper .env is loaded in script runner #9448

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/bundler.zig
Expand Up @@ -540,13 +540,7 @@ pub const Bundler = struct {
this.options.setProduction(true);
}

if (this.options.isTest() or this.env.isTest()) {
try this.env.load(dir, this.options.env.files, .@"test");
} else if (this.options.production) {
try this.env.load(dir, this.options.env.files, .production);
} else {
try this.env.load(dir, this.options.env.files, .development);
}
try this.runEnvLoaderLoad(dir);
},
.disable => {
this.env.loadProcess();
Expand All @@ -573,6 +567,16 @@ pub const Bundler = struct {
Analytics.disabled = Analytics.disabled or this.env.get("HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET") != null;
}

pub fn runEnvLoaderLoad(this: *Bundler, dir: *Fs.FileSystem.DirEntry) !void {
if (this.options.isTest() or this.env.isTest()) {
try this.env.load(dir, this.options.env.files, .@"test");
} else if (this.options.production) {
try this.env.load(dir, this.options.env.files, .production);
} else {
try this.env.load(dir, this.options.env.files, .development);
}
}

// This must be run after a framework is configured, if a framework is enabled
pub fn configureDefines(this: *Bundler) !void {
if (this.options.defines_loaded) {
Expand Down
31 changes: 3 additions & 28 deletions src/cli/run_command.zig
Expand Up @@ -764,7 +764,7 @@ pub const RunCommand = struct {

this_bundler.configureLinker();

var root_dir_info = this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch |err| {
const root_dir_info = this_bundler.resolver.readDirInfo(this_bundler.fs.top_level_dir) catch |err| {
if (!log_errors) return error.CouldntReadCurrentDirectory;
if (Output.enable_ansi_colors) {
ctx.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
Expand All @@ -789,27 +789,7 @@ pub const RunCommand = struct {

if (env == null) {
this_bundler.env.loadProcess();

if (this_bundler.env.get("NODE_ENV")) |node_env| {
if (strings.eqlComptime(node_env, "production")) {
this_bundler.options.production = true;
}
}

// TODO: evaluate if we can skip running this in nested calls to bun run
// The reason why it's unclear:
// - Some scripts may do NODE_ENV=production bun run foo
// This would cause potentially a different .env file to be loaded
this_bundler.runEnvLoader() catch {};

if (root_dir_info.getEntries(0)) |dir| {
// Run .env again if it exists in a parent dir
if (this_bundler.options.production) {
this_bundler.env.load(dir, this_bundler.options.env.files, .production) catch {};
} else {
this_bundler.env.load(dir, this_bundler.options.env.files, .development) catch {};
}
}
if (this_bundler.env.isProduction()) this_bundler.options.production = true;
}

this_bundler.env.map.putDefault("npm_config_local_prefix", this_bundler.fs.top_level_dir) catch unreachable;
Expand Down Expand Up @@ -968,12 +948,7 @@ pub const RunCommand = struct {

{
this_bundler.env.loadProcess();

if (this_bundler.env.get("NODE_ENV")) |node_env| {
if (strings.eqlComptime(node_env, "production")) {
this_bundler.options.production = true;
}
}
if (this_bundler.env.isProduction()) this_bundler.options.production = true;
}

const ResultList = bun.StringArrayHashMap(void);
Expand Down
2 changes: 1 addition & 1 deletion test/cli/install/dummy.registry.ts
Expand Up @@ -102,7 +102,7 @@ export function dummyAfterAll() {
server.stop();
}

let packageDirGetter: () => string = () => {
export let packageDirGetter: () => string = () => {
return tmpdirSync("bun-install-test-" + testCounter++ + "--");
};
export async function dummyBeforeEach() {
Expand Down
63 changes: 61 additions & 2 deletions test/js/bun/shell/bunshell.test.ts
Expand Up @@ -7,11 +7,12 @@
*/
import { $ } from "bun";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, realpath, rm } from "fs/promises";
import { bunEnv, runWithErrorPromise, tempDirWithFiles } from "harness";
import { mkdir, mkdtemp, realpath, rm, writeFile, readFile } from "fs/promises";
import { bunEnv, bunExe, runWithErrorPromise, tempDirWithFiles } from "harness";
import { tmpdir } from "os";
import { join, sep } from "path";
import { TestBuilder, sortedShellOutput } from "./util";
import { packageDirGetter } from "./../../../cli/install/dummy.registry";

$.env(bunEnv);
$.cwd(process.cwd());
Expand Down Expand Up @@ -831,3 +832,61 @@ function sentinelByte(buf: Uint8Array): number {
}
throw new Error("No sentinel byte");
}

test("shell as script runner propagates env vars: bun run <foo>", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be condensed into a test.each for the bun run permutation and the non-bun run permutation

const package_dir = packageDirGetter();
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({ scripts: { prod: `NODE_ENV=production ${$.escape(bunExe())} index.ts` } }),
);
await writeFile(
join(package_dir, "index.ts"),
`console.log(process.env.NODE_ENV); console.log(process.env.AWESOME);`,
);
await writeFile(join(package_dir, ".env.production"), `AWESOME=production`);
await writeFile(join(package_dir, ".env.development"), `AWESOME=development`);

const { stdout, stderr, exited } = Bun.spawn({
cmd: [bunExe(), "run", "prod"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd: [bunExe(), "run", "prod"],
cmd: [bunExe(), "--shell=bun", "run", "prod"],

cwd: package_dir,
stdio: ["pipe", "pipe", "pipe"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stdio: ["pipe", "pipe", "pipe"],
stdio: ["ignore", "pipe", "pipe"],

env: bunEnv,
});
expect(stderr).toBeDefined();
const err = await new Response(stderr).text();
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
expect(stdout).toBeDefined();
const out = await new Response(stdout).text();
expect(out.trim().split("\n")).toEqual(["production", "production"]);
expect(await exited).toBe(0);
});

test("shell as script runner propagates env vars: bun <foo>", async () => {
const package_dir = packageDirGetter();
await writeFile(
join(package_dir, "package.json"),
JSON.stringify({ scripts: { prod: `NODE_ENV=production ${$.escape(bunExe())} index.ts` } }),
);
await writeFile(
join(package_dir, "index.ts"),
`console.log(process.env.NODE_ENV); console.log(process.env.AWESOME);`,
);
await writeFile(join(package_dir, ".env.production"), `AWESOME=production`);
await writeFile(join(package_dir, ".env.development"), `AWESOME=development`);

const { stdout, stderr, exited } = Bun.spawn({
cmd: [bunExe(), "prod"],
cwd: package_dir,
stdio: ["pipe", "pipe", "pipe"],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stdio: ["pipe", "pipe", "pipe"],
stdio: ["ignore", "pipe", "pipe"],

env: bunEnv,
});
expect(stderr).toBeDefined();
const err = await new Response(stderr).text();
expect(err).not.toContain("error:");
expect(err).not.toContain("panic:");
expect(stdout).toBeDefined();
const out = await new Response(stdout).text();
expect(out.trim().split("\n")).toEqual(["production", "production"]);
expect(await exited).toBe(0);
});