mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
feat: add support --conditions flag (#9106)
* feat(options): add possibility to append a custom esm condition * feat(cli): parse --conditions flag * test: add case for custom conditions * fix(cli): not get short-hand --conditions flag * test: add case using cjs with custom condition * fix(options): address possible memory issues for esm conditions * refactor(cli): remove -c alias for --conditions flag * test: add cases for multiple --conditions specified * test(bundler): add support to test --conditions * chore(cli): fix grammar mistakes in --conditions --------- Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
@@ -1755,6 +1755,9 @@ pub const Api = struct {
|
||||
/// source_map
|
||||
source_map: ?SourceMapMode = null,
|
||||
|
||||
/// conditions
|
||||
conditions: []const []const u8,
|
||||
|
||||
pub fn decode(reader: anytype) anyerror!TransformOptions {
|
||||
var this = std.mem.zeroes(TransformOptions);
|
||||
|
||||
@@ -1839,6 +1842,9 @@ pub const Api = struct {
|
||||
25 => {
|
||||
this.source_map = try reader.readValue(SourceMapMode);
|
||||
},
|
||||
26 => {
|
||||
this.conditions = try reader.readArray([]const u8);
|
||||
},
|
||||
else => {
|
||||
return error.InvalidMessage;
|
||||
},
|
||||
@@ -1948,6 +1954,12 @@ pub const Api = struct {
|
||||
try writer.writeFieldID(25);
|
||||
try writer.writeEnum(source_map);
|
||||
}
|
||||
|
||||
if (this.conditions) |conditions| {
|
||||
try writer.writeFieldID(26);
|
||||
try writer.writeArray([]const u8, conditions);
|
||||
}
|
||||
|
||||
try writer.endMessage();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1617,6 +1617,7 @@ pub const BundleV2 = struct {
|
||||
.main_fields = &.{},
|
||||
.extension_order = &.{},
|
||||
.env_files = &.{},
|
||||
.conditions = &.{},
|
||||
},
|
||||
completion.env,
|
||||
);
|
||||
|
||||
@@ -182,6 +182,7 @@ pub const Arguments = struct {
|
||||
clap.parseParam("--prefer-latest Use the latest matching versions of packages in the Bun runtime, always checking npm") catch unreachable,
|
||||
clap.parseParam("-p, --port <STR> Set the default port for Bun.serve") catch unreachable,
|
||||
clap.parseParam("-u, --origin <STR>") catch unreachable,
|
||||
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
|
||||
};
|
||||
|
||||
const auto_only_params = [_]ParamType{
|
||||
@@ -229,6 +230,7 @@ pub const Arguments = struct {
|
||||
clap.parseParam("--minify-whitespace Minify whitespace") catch unreachable,
|
||||
clap.parseParam("--minify-identifiers Minify identifiers") catch unreachable,
|
||||
clap.parseParam("--dump-environment-variables") catch unreachable,
|
||||
clap.parseParam("--conditions <STR>... Pass custom conditions to resolve") catch unreachable,
|
||||
};
|
||||
pub const build_params = build_only_params ++ transpiler_params_ ++ base_params_;
|
||||
|
||||
@@ -516,6 +518,12 @@ pub const Arguments = struct {
|
||||
|
||||
ctx.passthrough = args.remaining();
|
||||
|
||||
if (cmd == .AutoCommand or cmd == .RunCommand or cmd == .BuildCommand) {
|
||||
if (args.options("--conditions").len > 0) {
|
||||
opts.conditions = args.options("--conditions");
|
||||
}
|
||||
}
|
||||
|
||||
// runtime commands
|
||||
if (cmd == .AutoCommand or cmd == .RunCommand or cmd == .TestCommand or cmd == .RunAsNodeCommand) {
|
||||
const preloads = args.options("--preload");
|
||||
|
||||
@@ -932,6 +932,18 @@ pub const ESMConditions = struct {
|
||||
.require = require_condition_map,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn appendSlice(self: *ESMConditions, conditions: []const string) !void {
|
||||
try self.default.ensureUnusedCapacity(conditions.len);
|
||||
try self.import.ensureUnusedCapacity(conditions.len);
|
||||
try self.require.ensureUnusedCapacity(conditions.len);
|
||||
|
||||
for (conditions) |condition| {
|
||||
self.default.putAssumeCapacityNoClobber(condition, {});
|
||||
self.import.putAssumeCapacityNoClobber(condition, {});
|
||||
self.require.putAssumeCapacityNoClobber(condition, {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const JSX = struct {
|
||||
@@ -1685,6 +1697,10 @@ pub const BundleOptions = struct {
|
||||
|
||||
opts.conditions = try ESMConditions.init(allocator, Target.DefaultConditions.get(opts.target));
|
||||
|
||||
if (transform.conditions.len > 0) {
|
||||
opts.conditions.appendSlice(transform.conditions) catch bun.outOfMemory();
|
||||
}
|
||||
|
||||
switch (opts.target) {
|
||||
.node => {
|
||||
opts.import_path_format = .relative;
|
||||
|
||||
@@ -1381,8 +1381,9 @@ describe("bundler", () => {
|
||||
"/Users/user/project/node_modules/pkg1/custom2.js": `console.log('SUCCESS')`,
|
||||
},
|
||||
outfile: "/Users/user/project/out.js",
|
||||
bundleErrors: {
|
||||
"/Users/user/project/src/entry.js": [`Could not resolve: "pkg1". Maybe you need to "bun install"?`],
|
||||
conditions: ["custom2"],
|
||||
run: {
|
||||
stdout: "SUCCESS",
|
||||
},
|
||||
});
|
||||
itBundled("packagejson/ExportsNotExactMissingExtension", {
|
||||
|
||||
@@ -147,6 +147,10 @@ export interface BundlerTestInput {
|
||||
assetNaming?: string;
|
||||
banner?: string;
|
||||
define?: Record<string, string | number>;
|
||||
|
||||
/** Use for resolve custom conditions */
|
||||
conditions?: String[];
|
||||
|
||||
/** Default is "[name].[ext]" */
|
||||
entryNaming?: string;
|
||||
/** Default is "[name]-[hash].[ext]" */
|
||||
@@ -397,6 +401,7 @@ function expectBundled(
|
||||
unsupportedCSSFeatures,
|
||||
unsupportedJSFeatures,
|
||||
useDefineForClassFields,
|
||||
conditions,
|
||||
// @ts-expect-error
|
||||
_referenceFn,
|
||||
...unknownProps
|
||||
@@ -580,6 +585,7 @@ function expectBundled(
|
||||
`--target=${target}`,
|
||||
// `--format=${format}`,
|
||||
external && external.map(x => ["--external", x]),
|
||||
conditions && conditions.map(x => ["--conditions", x]),
|
||||
minifyIdentifiers && `--minify-identifiers`,
|
||||
minifySyntax && `--minify-syntax`,
|
||||
minifyWhitespace && `--minify-whitespace`,
|
||||
@@ -616,6 +622,7 @@ function expectBundled(
|
||||
minifyWhitespace && `--minify-whitespace`,
|
||||
globalName && `--global-name=${globalName}`,
|
||||
external && external.map(x => `--external:${x}`),
|
||||
conditions && `--conditions=${conditions.join(",")}`,
|
||||
inject && inject.map(x => `--inject:${path.join(root, x)}`),
|
||||
define && Object.entries(define).map(([k, v]) => `--define:${k}=${v}`),
|
||||
`--jsx=${jsx.runtime === "classic" ? "transform" : "automatic"}`,
|
||||
|
||||
133
test/js/bun/resolve/import-custom-condition.test.ts
Normal file
133
test/js/bun/resolve/import-custom-condition.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { it, expect, beforeAll } from "bun:test";
|
||||
import { writeFileSync } from "fs";
|
||||
import { bunExe, bunEnv, tempDirWithFiles } from "harness";
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeAll(() => {
|
||||
dir = tempDirWithFiles("customcondition", {
|
||||
"./node_modules/custom/index.js": "export const foo = 1;",
|
||||
"./node_modules/custom/not_allow.js": "throw new Error('should not be imported')",
|
||||
"./node_modules/custom/package.json": JSON.stringify({
|
||||
name: "custom",
|
||||
exports: {
|
||||
"./test": {
|
||||
first: "./index.js",
|
||||
default: "./not_allow.js",
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
"./node_modules/custom2/index.cjs": "module.exports.foo = 5;",
|
||||
"./node_modules/custom2/index.mjs": "export const foo = 1;",
|
||||
"./node_modules/custom2/not_allow.js": "throw new Error('should not be imported')",
|
||||
"./node_modules/custom2/package.json": JSON.stringify({
|
||||
name: "custom2",
|
||||
exports: {
|
||||
"./test": {
|
||||
first: {
|
||||
import: "./index.mjs",
|
||||
require: "./index.cjs",
|
||||
default: "./index.mjs",
|
||||
},
|
||||
default: "./not_allow.js",
|
||||
},
|
||||
"./test2": {
|
||||
second: {
|
||||
import: "./index.mjs",
|
||||
require: "./index.cjs",
|
||||
default: "./index.mjs",
|
||||
},
|
||||
default: "./not_allow.js",
|
||||
},
|
||||
"./test3": {
|
||||
third: {
|
||||
import: "./index.mjs",
|
||||
require: "./index.cjs",
|
||||
default: "./index.mjs",
|
||||
},
|
||||
default: "./not_allow.js",
|
||||
},
|
||||
},
|
||||
type: "module",
|
||||
}),
|
||||
});
|
||||
|
||||
writeFileSync(`${dir}/test.js`, `import {foo} from 'custom/test';\nconsole.log(foo);`);
|
||||
writeFileSync(`${dir}/test.cjs`, `const {foo} = require("custom2/test");\nconsole.log(foo);`);
|
||||
writeFileSync(
|
||||
`${dir}/multiple-conditions.js`,
|
||||
`const pkg1 = require("custom2/test");\nconst pkg2 = require("custom2/test2");\nconst pkg3 = require("custom2/test3");\nconsole.log(pkg1.foo, pkg2.foo, pkg3.foo);`,
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
`${dir}/package.json`,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "hello",
|
||||
imports: {
|
||||
custom: "custom",
|
||||
custom2: "custom2",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("custom condition 'import' in package.json resolves", async () => {
|
||||
const { exitCode, stdout } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "--conditions=first", `${dir}/test.js`],
|
||||
env: bunEnv,
|
||||
cwd: import.meta.dir,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toString("utf8")).toBe("1\n");
|
||||
});
|
||||
|
||||
it("custom condition 'require' in package.json resolves", async () => {
|
||||
const { exitCode, stdout } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "--conditions=first", `${dir}/test.cjs`],
|
||||
env: bunEnv,
|
||||
cwd: import.meta.dir,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toString("utf8")).toBe("5\n");
|
||||
});
|
||||
|
||||
it("multiple conditions in package.json resolves", async () => {
|
||||
const { exitCode, stdout } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "--conditions=first", "--conditions=second", "--conditions=third", `${dir}/multiple-conditions.js`],
|
||||
env: bunEnv,
|
||||
cwd: import.meta.dir,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout.toString("utf8")).toBe("5 5 5\n");
|
||||
});
|
||||
|
||||
it("multiple conditions when some not specified should resolves to fallback", async () => {
|
||||
const { exitCode, stderr } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "--conditions=first", "--conditions=second", `${dir}/multiple-conditions.js`],
|
||||
env: bunEnv,
|
||||
cwd: import.meta.dir,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
|
||||
// not_allow.js is the fallback for third condition, so it should be in stderr
|
||||
expect(stderr.toString("utf8")).toMatch("new Error('should not be imported')");
|
||||
});
|
||||
|
||||
it("custom condition when don't match condition should resolves to default", async () => {
|
||||
const { exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "--conditions=first1", `${dir}/test.js`],
|
||||
env: bunEnv,
|
||||
cwd: import.meta.dir,
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user