aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-09-19 20:03:02 -0700
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-09-19 20:03:02 -0700
commit21770eb0f31a43b3d6127ab957e271d029d6bc1b (patch)
tree1f8944dae891af25bb1b2cd923d3c47d5845d14d /src
parent17be8023d9a34d985000af9211bdac8effa5396e (diff)
downloadbun-21770eb0f31a43b3d6127ab957e271d029d6bc1b.tar.gz
bun-21770eb0f31a43b3d6127ab957e271d029d6bc1b.tar.zst
bun-21770eb0f31a43b3d6127ab957e271d029d6bc1b.zip
Implement `imports` in package.json (`#foo` imports)
Fixes https://github.com/oven-sh/bun/issues/478 Fixes https://github.com/oven-sh/bun/issues/234 Fixes https://github.com/oven-sh/bun/issues/822
Diffstat (limited to 'src')
-rw-r--r--src/resolver/package_json.zig99
-rw-r--r--src/resolver/resolver.zig191
2 files changed, 205 insertions, 85 deletions
diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig
index 69dc2e8dc..e33c5ac60 100644
--- a/src/resolver/package_json.zig
+++ b/src/resolver/package_json.zig
@@ -113,6 +113,7 @@ pub const PackageJSON = struct {
browser_map: BrowserMap,
exports: ?ExportsMap = null,
+ imports: ?ExportsMap = null,
pub inline fn isAppPackage(this: *const PackageJSON) bool {
return this.hash == 0xDEADBEEF;
@@ -709,6 +710,12 @@ pub const PackageJSON = struct {
}
}
+ if (json.asProperty("imports")) |imports_prop| {
+ if (ExportsMap.parse(r.allocator, &json_source, r.log, imports_prop.expr)) |imports_map| {
+ package_json.imports = imports_map;
+ }
+ }
+
// used by `bun run`
if (include_scripts) {
read_scripts: {
@@ -1000,27 +1007,32 @@ pub const ESModule = struct {
Exact,
Inexact, // This means we may need to try CommonJS-style extension suffixes
- // Module specifier is an invalid URL, package name or package subpath specifier.
+ /// Module specifier is an invalid URL, package name or package subpath specifier.
InvalidModuleSpecifier,
- // package.json configuration is invalid or contains an invalid configuration.
+ /// package.json configuration is invalid or contains an invalid configuration.
InvalidPackageConfiguration,
- // Package exports or imports define a target module for the package that is an invalid type or string target.
+ /// Package exports or imports define a target module for the package that is an invalid type or string target.
InvalidPackageTarget,
- // Package exports do not define or permit a target subpath in the package for the given module.
+ /// Package exports do not define or permit a target subpath in the package for the given module.
PackagePathNotExported,
- // The package or module requested does not exist.
+ /// The package or module requested does not exist.
ModuleNotFound,
- // The resolved path corresponds to a directory, which is not a supported target for module imports.
+ /// The resolved path corresponds to a directory, which is not a supported target for module imports.
UnsupportedDirectoryImport,
- // When a package path is explicitly set to null, that means it's not exported.
+ /// When a package path is explicitly set to null, that means it's not exported.
PackagePathDisabled,
+ // The internal #import specifier was not found
+ PackageImportNotDefined,
+
+ PackageResolve,
+
pub inline fn isUndefined(this: Status) bool {
return switch (this) {
.Undefined, .UndefinedNoConditionsMatch => true,
@@ -1075,8 +1087,40 @@ pub const ESModule = struct {
threadlocal var resolved_path_buf_percent: [bun.MAX_PATH_BYTES]u8 = undefined;
pub fn resolve(r: *const ESModule, package_url: string, subpath: string, exports: ExportsMap.Entry) Resolution {
- var result = r.resolveExports(package_url, subpath, exports);
+ return finalize(
+ r.resolveExports(package_url, subpath, exports),
+ );
+ }
+ pub fn resolveImports(r: *const ESModule, specifier: string, imports: ExportsMap.Entry) Resolution {
+ if (imports.data != .map) {
+ return .{
+ .status = .InvalidPackageConfiguration,
+ .debug = .{
+ .token = logger.Range.None,
+ },
+ };
+ }
+
+ const result = r.resolveImportsExports(
+ specifier,
+ imports,
+ true,
+ "/",
+ );
+
+ switch (result.status) {
+ .Undefined, .Null => {
+ return .{ .status = .PackageImportNotDefined, .debug = .{ .token = result.debug.token } };
+ },
+ else => {
+ return finalize(result);
+ },
+ }
+ }
+
+ pub fn finalize(result_: Resolution) Resolution {
+ var result = result_;
if (result.status != .Exact and result.status != .Inexact) {
return result;
}
@@ -1149,13 +1193,13 @@ pub const ESModule = struct {
}
if (main_export.data != .@"null") {
- const result = r.resolveTarget(package_url, main_export, "", false);
+ const result = r.resolveTarget(package_url, main_export, "", false, false);
if (result.status != .Null and result.status != .Undefined) {
return result;
}
}
} else if (exports.data == .map and exports.keysStartWithDot()) {
- const result = r.resolveImportsExports(subpath, exports, package_url);
+ const result = r.resolveImportsExports(subpath, exports, false, package_url);
if (result.status != .Null and result.status != .Undefined) {
return result;
}
@@ -1176,6 +1220,7 @@ pub const ESModule = struct {
r: *const ESModule,
match_key: string,
match_obj: ExportsMap.Entry,
+ is_imports: bool,
package_url: string,
) Resolution {
if (r.debug_logs) |logs| {
@@ -1188,7 +1233,7 @@ pub const ESModule = struct {
log.addNoteFmt("Found \"{s}\"", .{match_key}) catch unreachable;
}
- return r.resolveTarget(package_url, target, "", false);
+ return r.resolveTarget(package_url, target, "", is_imports, false);
}
}
@@ -1206,7 +1251,7 @@ pub const ESModule = struct {
log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath }) catch unreachable;
}
- return r.resolveTarget(package_url, target, subpath, true);
+ return r.resolveTarget(package_url, target, subpath, is_imports, true);
}
}
@@ -1217,7 +1262,7 @@ pub const ESModule = struct {
log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath }) catch unreachable;
}
- var result = r.resolveTarget(package_url, target, subpath, false);
+ var result = r.resolveTarget(package_url, target, subpath, is_imports, false);
result.status = if (result.status == .Exact)
// Return the object { resolved, exact: false }.
.Inexact
@@ -1250,6 +1295,7 @@ pub const ESModule = struct {
package_url: string,
target: ExportsMap.Entry,
subpath: string,
+ internal: bool,
comptime pattern: bool,
) Resolution {
switch (target.data) {
@@ -1276,11 +1322,34 @@ pub const ESModule = struct {
}
}
+ // If target does not start with "./", then...
if (!strings.startsWith(str, "./")) {
if (r.debug_logs) |log| {
log.addNoteFmt("The target \"{s}\" is invalid because it doesn't start with a \"./\"", .{str}) catch unreachable;
}
+ if (internal and !strings.hasPrefixComptime(str, "../") and !strings.hasPrefix(str, "/")) {
+ if (comptime pattern) {
+ // Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
+ const len = std.mem.replacementSize(u8, str, "*", subpath);
+ _ = std.mem.replace(u8, str, "*", subpath, &resolve_target_buf2);
+ const result = resolve_target_buf2[0..len];
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("Subsituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, str, result }) catch unreachable;
+ }
+
+ return Resolution{ .path = result, .status = .PackageResolve, .debug = .{ .token = target.first_token } };
+ } else {
+ var parts2 = [_]string{ str, subpath };
+ const result = resolve_path.joinStringBuf(&resolve_target_buf2, parts2, .auto);
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("Resolved \".{s}\" to \".{s}\"", .{ str, result }) catch unreachable;
+ }
+
+ return Resolution{ .path = result, .status = .PackageResolve, .debug = .{ .token = target.first_token } };
+ }
+ }
+
return Resolution{ .path = str, .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
}
@@ -1340,7 +1409,7 @@ pub const ESModule = struct {
log.addNoteFmt("The key \"{s}\" matched", .{key}) catch unreachable;
}
- var result = r.resolveTarget(package_url, slice.items(.value)[i], subpath, pattern);
+ var result = r.resolveTarget(package_url, slice.items(.value)[i], subpath, internal, pattern);
if (result.status.isUndefined()) {
did_find_map_entry = true;
last_map_entry_i = i;
@@ -1431,7 +1500,7 @@ pub const ESModule = struct {
for (array) |targetValue| {
// Let resolved be the result, continuing the loop on any Invalid Package Target error.
- const result = r.resolveTarget(package_url, targetValue, subpath, pattern);
+ const result = r.resolveTarget(package_url, targetValue, subpath, internal, pattern);
if (result.status == .InvalidPackageTarget or result.status == .Null) {
last_debug = result.debug;
last_exception = result.status;
diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig
index 93cdec82e..6496ce118 100644
--- a/src/resolver/resolver.zig
+++ b/src/resolver/resolver.zig
@@ -1007,7 +1007,7 @@ pub const Resolver = struct {
if (remapped.len == 0) {
// "browser": {"module": false}
// does the module exist in the filesystem?
- if (r.loadNodeModules(import_path, kind, source_dir_info)) |node_module| {
+ if (r.loadNodeModules(import_path, kind, source_dir_info, false)) |node_module| {
var pair = node_module.path_pair;
pair.primary.is_disabled = true;
if (pair.secondary != null) {
@@ -1183,7 +1183,13 @@ pub const Resolver = struct {
threadlocal var esm_subpath_buf: [512]u8 = undefined;
threadlocal var esm_absolute_package_path: [bun.MAX_PATH_BYTES]u8 = undefined;
threadlocal var esm_absolute_package_path_joined: [bun.MAX_PATH_BYTES]u8 = undefined;
- pub fn loadNodeModules(r: *ThisResolver, import_path: string, kind: ast.ImportKind, _dir_info: *DirInfo) ?MatchResult {
+ pub fn loadNodeModules(
+ r: *ThisResolver,
+ import_path: string,
+ kind: ast.ImportKind,
+ _dir_info: *DirInfo,
+ forbid_imports: bool,
+ ) ?MatchResult {
var dir_info = _dir_info;
if (r.debug_logs) |*debug| {
debug.addNoteFmt("Searching for {s} in \"node_modules\" directories starting from \"{s}\"", .{ import_path, dir_info.abs_path }) catch {};
@@ -1221,6 +1227,44 @@ pub const Resolver = struct {
const esm_ = ESModule.Package.parse(import_path, &esm_subpath_buf);
+ if (import_path[0] == '#' and !forbid_imports) {
+ if (esm_ != null) {
+ if (dir_info.package_json) |package_json| {
+ load_from_imports_map: {
+ const imports_map = package_json.imports orelse break :load_from_imports_map;
+
+ if (import_path.len == 1 or strings.hasPrefix(import_path, "#/")) {
+ if (r.debug_logs) |*debug| {
+ debug.addNoteFmt("The path \"{s}\" must not equal \"#\" and must not start with \"#/\"", .{import_path}) catch {};
+ }
+ return null;
+ }
+
+ const esmodule = ESModule{
+ .conditions = switch (kind) {
+ ast.ImportKind.require, ast.ImportKind.require_resolve => r.opts.conditions.require,
+ else => r.opts.conditions.import,
+ },
+ .allocator = r.allocator,
+ .debug_logs = if (r.debug_logs) |*debug| debug else null,
+ };
+
+ const esm_resolution = esmodule.resolveImports(import_path, imports_map.root);
+
+ if (esm_resolution.status == .PackageResolve)
+ return r.loadNodeModules(
+ esm_resolution.path,
+ kind,
+ dir_info,
+ true,
+ );
+
+ return r.handleESMResolution(esm_resolution, dir_info.abs_path, kind, package_json);
+ }
+ }
+ }
+ }
+
// Then check for the package in any enclosing "node_modules" directories
while (true) {
// Skip directories that are themselves called "node_modules", since we
@@ -1259,73 +1303,10 @@ pub const Resolver = struct {
// 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.
- var esm_resolution = esmodule.resolve("/", esm.subpath, exports_map.root);
-
- if ((esm_resolution.status == .Inexact or esm_resolution.status == .Exact) and
- esm_resolution.path.len > 0 and esm_resolution.path[0] == '/')
- {
- 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(std.fs.path.dirname(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;
- }
-
- const absolute_out_path = brk: {
- if (entry_query.entry.abs_path.isEmpty()) {
- entry_query.entry.abs_path =
- PathString.init(r.fs.dirname_store.append(@TypeOf(abs_esm_path), abs_esm_path) catch unreachable);
- }
- break :brk entry_query.entry.abs_path.slice();
- };
-
- return MatchResult{
- .path_pair = PathPair{
- .primary = Path.initWithNamespace(absolute_out_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 = resolved_dir_info.package_json orelse 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| {
- res.is_node_module = true;
- res.package_json = res.package_json orelse package_json;
- return res.*;
- }
- esm_resolution.status = .ModuleNotFound;
- return null;
- },
- else => unreachable,
- }
+ const esm_resolution = esmodule.resolve("/", esm.subpath, exports_map.root);
+
+ if (r.handleESMResolution(esm_resolution, abs_package_path, kind, package_json)) |res| {
+ return res;
}
}
}
@@ -1347,9 +1328,79 @@ pub const Resolver = struct {
return null;
}
+ fn handleESMResolution(r: *ThisResolver, esm_resolution_: ESModule.Resolution, abs_package_path: string, kind: ast.ImportKind, package_json: *PackageJSON) ?MatchResult {
+ var esm_resolution = esm_resolution_;
+ if (!((esm_resolution.status == .Inexact or esm_resolution.status == .Exact) and
+ esm_resolution.path.len > 0 and esm_resolution.path[0] == '/'))
+ return null;
+
+ 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(std.fs.path.dirname(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;
+ }
+
+ const absolute_out_path = brk: {
+ if (entry_query.entry.abs_path.isEmpty()) {
+ entry_query.entry.abs_path =
+ PathString.init(r.fs.dirname_store.append(@TypeOf(abs_esm_path), abs_esm_path) catch unreachable);
+ }
+ break :brk entry_query.entry.abs_path.slice();
+ };
+
+ return MatchResult{
+ .path_pair = PathPair{
+ .primary = Path.initWithNamespace(absolute_out_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 = resolved_dir_info.package_json orelse 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| {
+ res.is_node_module = true;
+ res.package_json = res.package_json orelse package_json;
+ return res.*;
+ }
+ esm_resolution.status = .ModuleNotFound;
+ return null;
+ },
+ else => unreachable,
+ }
+ }
+
pub fn resolveWithoutRemapping(r: *ThisResolver, source_dir_info: *DirInfo, import_path: string, kind: ast.ImportKind) ?MatchResult {
if (isPackagePath(import_path)) {
- return r.loadNodeModules(import_path, kind, source_dir_info);
+ return r.loadNodeModules(import_path, kind, source_dir_info, false);
} else {
const paths = [_]string{ source_dir_info.abs_path, import_path };
var resolved = r.fs.absBuf(&paths, &resolve_without_remapping_buf);