aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/options.zig99
-rw-r--r--src/resolver/resolver.zig96
2 files changed, 194 insertions, 1 deletions
diff --git a/src/options.zig b/src/options.zig
index 02d44fa08..5e1417495 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -11,6 +11,7 @@ const defines = @import("./defines.zig");
const resolve_path = @import("./resolver/resolve_path.zig");
const NodeModuleBundle = @import("./node_module_bundle.zig").NodeModuleBundle;
const URL = @import("./query_string_map.zig").URL;
+const ConditionsMap = @import("./resolver/package_json.zig").ESModule.ConditionsMap;
usingnamespace @import("global.zig");
const DotEnv = @import("./env_loader.zig");
@@ -456,6 +457,67 @@ pub const Platform = enum {
break :brk array;
};
+
+ pub const default_conditions_strings = .{
+ .browser = @as("browser", string),
+ .import = @as("import", string),
+ .require = @as("require", string),
+ .node = @as("node", string),
+ .default = @as("default", string),
+ .bun = @as("bun", string),
+ .bun_macro = @as("bun_macro", string),
+ };
+
+ pub const DefaultConditions: std.EnumArray(Platform, []const string) = brk: {
+ var array = std.EnumArray(Platform, []const string).initUndefined();
+
+ // Note that this means if a package specifies "module" and "main", the ES6
+ // module will not be selected. This means tree shaking will not work when
+ // targeting node environments.
+ //
+ // This is unfortunately necessary for compatibility. Some packages
+ // incorrectly treat the "module" field as "code for the browser". It
+ // actually means "code for ES6 environments" which includes both node
+ // and the browser.
+ //
+ // For example, the package "@firebase/app" prints a warning on startup about
+ // the bundler incorrectly using code meant for the browser if the bundler
+ // selects the "module" field instead of the "main" field.
+ //
+ // If you want to enable tree shaking when targeting node, you will have to
+ // configure the main fields to be "module" and then "main". Keep in mind
+ // that some packages may break if you do this.
+ array.set(Platform.node, [_]string{default_conditions_strings.node});
+
+ // Note that this means if a package specifies "main", "module", and
+ // "browser" then "browser" will win out over "module". This is the
+ // same behavior as webpack: https://github.com/webpack/webpack/issues/4674.
+ //
+ // This is deliberate because the presence of the "browser" field is a
+ // good signal that the "module" field may have non-browser stuff in it,
+ // which will crash or fail to be bundled when targeting the browser.
+ var listc = [_]string{
+ default_conditions_strings.browser,
+ };
+ array.set(Platform.browser, &listc);
+ array.set(
+ Platform.bun,
+ [_]string{
+ default_conditions_strings.bun,
+ default_conditions_strings.browser,
+ },
+ );
+ // array.set(Platform.bun_macro, [_]string{ default_conditions_strings.bun_macro, default_conditions_strings.browser, default_conditions_strings.default, },);
+
+ // Original comment:
+ // The neutral platform is for people that don't want esbuild to try to
+ // pick good defaults for their platform. In that case, the list of main
+ // fields is empty by default. You must explicitly configure it yourself.
+
+ array.set(Platform.neutral, &listc);
+
+ break :brk array;
+ };
};
pub const Loader = enum(u3) {
@@ -525,6 +587,39 @@ pub const defaultLoaders = std.ComptimeStringMap(Loader, .{
.{ ".tsx", Loader.tsx },
});
+pub const ESMConditions = struct {
+ default: ConditionsMap = undefined,
+ import: ConditionsMap = undefined,
+ require: ConditionsMap = undefined,
+
+ pub fn init(allocator: *std.mem.Allocator, defaults: []const string) !ESMConditions {
+ var default_condition_amp = ConditionsMap.init(allocator);
+
+ var import_condition_map = ConditionsMap.init(allocator);
+ var require_condition_map = ConditionsMap.init(allocator);
+
+ try default_condition_amp.ensureTotalCapacity(defaults.len + 1);
+ try import_condition_map.ensureTotalCapacity(defaults.len + 1);
+ try require_condition_map.ensureTotalCapacity(defaults.len + 1);
+
+ import_condition_map.putAssumeCapacityNoClobber(Platform.default_conditions_strings.import, void{});
+ require_condition_map.putAssumeCapacityNoClobber(Platform.default_conditions_strings.require, void{});
+ default_condition_amp.putAssumeCapacityNoClobber(Platform.default_conditions_strings.default, void{});
+
+ for (defaults) |default| {
+ default_condition_amp.putAssumeCapacityNoClobber(default, void{});
+ import_condition_map.putAssumeCapacityNoClobber(default, void{});
+ require_condition_map.putAssumeCapacityNoClobber(default, void{});
+ }
+
+ return ESMConditions{
+ .default = default_condition_amp,
+ .import = import_condition_map,
+ .require = require_condition_map,
+ };
+ }
+};
+
pub const JSX = struct {
pub const Pragma = struct {
// these need to be arrays
@@ -827,6 +922,8 @@ pub const BundleOptions = struct {
transform_options: Api.TransformOptions,
polyfill_node_globals: bool = true,
+ conditions: ESMConditions = undefined,
+
pub inline fn cssImportBehavior(this: *const BundleOptions) Api.CssInJsBehavior {
switch (this.platform) {
.neutral, .browser => {
@@ -935,6 +1032,8 @@ pub const BundleOptions = struct {
opts.main_fields = Platform.DefaultMainFields.get(opts.platform);
}
+ opts.conditions = try ESMConditions.init(allocator, Platform.DefaultConditions.get(opts.platform));
+
if (transform.serve orelse false) {
// When we're serving, we need some kind of URL.
if (!opts.origin.isAbsolute()) {
diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig
index 4976d8ae1..8eb9c99de 100644
--- a/src/resolver/resolver.zig
+++ b/src/resolver/resolver.zig
@@ -8,6 +8,7 @@ const cache = @import("../cache.zig");
const sync = @import("../sync.zig");
const TSConfigJSON = @import("./tsconfig_json.zig").TSConfigJSON;
const PackageJSON = @import("./package_json.zig").PackageJSON;
+const ESModule = @import("./package_json.zig").ESModule;
const BrowserMap = @import("./package_json.zig").BrowserMap;
usingnamespace @import("./data_url.zig");
@@ -1119,6 +1120,9 @@ pub fn NewResolver(cache_files: bool) type {
return res;
}
+ threadlocal var esm_subpath_buf: [512]u8 = undefined;
+ threadlocal var esm_absolute_package_path: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+ threadlocal var esm_absolute_package_path_joined: [std.fs.MAX_PATH_BYTES]u8 = undefined;
inline fn _loadNodeModules(r: *ThisResolver, import_path: string, kind: ast.ImportKind, _dir_info: *DirInfo) ?MatchResult {
var dir_info = _dir_info;
if (r.debug_logs) |*debug| {
@@ -1155,6 +1159,8 @@ pub fn NewResolver(cache_files: bool) type {
}
}
+ const esm_ = ESModule.Package.parse(import_path, &esm_subpath_buf);
+
// Then check for the package in any enclosing "node_modules" directories
while (true) {
// Skip directories that are themselves called "node_modules", since we
@@ -1166,7 +1172,95 @@ pub fn NewResolver(cache_files: bool) type {
debug.addNoteFmt("Checking for a package in the directory \"{s}\"", .{abs_path}) catch {};
}
- // TODO: esm "exports" field goes here!!! Here!!
+ if (esm_) |esm| {
+ const abs_package_path = brk: {
+ var parts = [_]string{ dir_info.abs_path, "node_modules", esm.name };
+ break :brk r.fs.absBuf(&parts, &esm_absolute_package_path);
+ };
+
+ if (r.dirInfoCached(abs_package_path) catch null) |pkg_dir_info| {
+ if (pkg_dir_info.package_json) |package_json| {
+ if (package_json.exports) |exports_map| {
+
+ // The condition set is determined by the kind of import
+
+ // Resolve against the path "/", then join it with the absolute
+ // directory path. This is done because ESM package resolution uses
+ // URLs while our path resolution uses file system paths. We don't
+ // want problems due to Windows paths, which are very unlike URL
+ // paths. We also want to avoid any "%" characters in the absolute
+ // directory path accidentally being interpreted as URL escapes.
+ const esmodule = ESModule{
+ .conditions = switch (kind) {
+ ast.ImportKind.stmt, ast.ImportKind.dynamic => r.opts.conditions.import,
+ ast.ImportKind.require, ast.ImportKind.require_resolve => r.opts.conditions.require,
+ else => r.opts.conditions.default,
+ },
+ .allocator = r.allocator,
+ .debug_logs = &r.debug_logs,
+ };
+
+ var esm_resolution = esmodule.resolve("/", esm.subpath, exports_map.root);
+ defer Output.debug("ESM Resolution Status {s}: {s}\n", .{ abs_package_path, esm_resolution.status });
+
+ if ((esm_resolution.status == .Inexact or esm_resolution.status == .Exact) and strings.startsWith(esm_resolution.path, "/")) {
+ const abs_esm_path: string = brk: {
+ var parts = [_]string{
+ abs_package_path,
+ esm_resolution.path[1..],
+ };
+ break :brk r.fs.absBuf(&parts, &esm_absolute_package_path_joined);
+ };
+
+ switch (esm_resolution.status) {
+ .Exact => {
+ const resolved_dir_info = (r.dirInfoCached(abs_esm_path) catch null) orelse {
+ esm_resolution.status = .ModuleNotFound;
+ return null;
+ };
+ const entries = resolved_dir_info.getEntries() orelse {
+ esm_resolution.status = .ModuleNotFound;
+ return null;
+ };
+ const entry_query = entries.get(std.fs.path.basename(abs_esm_path)) orelse {
+ esm_resolution.status = .ModuleNotFound;
+ return null;
+ };
+
+ if (entry_query.entry.kind(&r.fs.fs) == .dir) {
+ esm_resolution.status = .UnsupportedDirectoryImport;
+ return null;
+ }
+
+ return MatchResult{
+ .path_pair = PathPair{
+ .primary = Path.initWithNamespace(esm_resolution.path, "file"),
+ },
+ .dirname_fd = entries.fd,
+ .file_fd = entry_query.entry.cache.fd,
+ .dir_info = resolved_dir_info,
+ .diff_case = entry_query.diff_case,
+ .is_node_module = true,
+ .package_json = package_json,
+ };
+ },
+ .Inexact => {
+ // If this was resolved against an expansion key ending in a "/"
+ // instead of a "*", we need to try CommonJS-style implicit
+ // extension and/or directory detection.
+ if (r.loadAsFileOrDirectory(abs_esm_path, kind)) |res| {
+ return res;
+ }
+ esm_resolution.status = .ModuleNotFound;
+ return null;
+ },
+ else => unreachable,
+ }
+ }
+ }
+ }
+ }
+ }
if (r.loadAsFileOrDirectory(abs_path, kind)) |res| {
return res;