diff options
-rw-r--r-- | src/options.zig | 99 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 96 |
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; |