From dcf6f244a4033f5793cf86ce8ea597610ea208aa Mon Sep 17 00:00:00 2001 From: Igor Wessel Date: Fri, 1 Mar 2024 23:57:45 -0300 Subject: [PATCH] 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 --- src/api/schema.zig | 12 ++ src/bundler/bundle_v2.zig | 1 + src/cli.zig | 8 ++ src/options.zig | 16 +++ test/bundler/esbuild/packagejson.test.ts | 5 +- test/bundler/expectBundled.ts | 7 + .../resolve/import-custom-condition.test.ts | 133 ++++++++++++++++++ 7 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 test/js/bun/resolve/import-custom-condition.test.ts diff --git a/src/api/schema.zig b/src/api/schema.zig index 998b2963da..0098a377c4 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -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(); } }; diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index ccbccab4ad..6e25f986b4 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -1617,6 +1617,7 @@ pub const BundleV2 = struct { .main_fields = &.{}, .extension_order = &.{}, .env_files = &.{}, + .conditions = &.{}, }, completion.env, ); diff --git a/src/cli.zig b/src/cli.zig index 030f24afe1..3a00c082a7 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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 Set the default port for Bun.serve") catch unreachable, clap.parseParam("-u, --origin ") catch unreachable, + clap.parseParam("--conditions ... 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 ... 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"); diff --git a/src/options.zig b/src/options.zig index 309cf858fa..f21f0124ef 100644 --- a/src/options.zig +++ b/src/options.zig @@ -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; diff --git a/test/bundler/esbuild/packagejson.test.ts b/test/bundler/esbuild/packagejson.test.ts index 5d459b968a..8b168b9db1 100644 --- a/test/bundler/esbuild/packagejson.test.ts +++ b/test/bundler/esbuild/packagejson.test.ts @@ -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", { diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 44b2fa6df3..f32d612d47 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -147,6 +147,10 @@ export interface BundlerTestInput { assetNaming?: string; banner?: string; define?: Record; + + /** 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"}`, diff --git a/test/js/bun/resolve/import-custom-condition.test.ts b/test/js/bun/resolve/import-custom-condition.test.ts new file mode 100644 index 0000000000..77b4110980 --- /dev/null +++ b/test/js/bun/resolve/import-custom-condition.test.ts @@ -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); +});