aboutsummaryrefslogtreecommitdiff
path: root/src/resolver/resolver.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/resolver/resolver.zig')
-rw-r--r--src/resolver/resolver.zig357
1 files changed, 345 insertions, 12 deletions
diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig
index 138b1b72f..164002b80 100644
--- a/src/resolver/resolver.zig
+++ b/src/resolver/resolver.zig
@@ -4,7 +4,15 @@ const logger = @import("../logger.zig");
const options = @import("../options.zig");
const fs = @import("../fs.zig");
const std = @import("std");
+const cache = @import("../cache.zig");
+const TSConfigJSON = @import("./tsconfig_json.zig").TSConfigJSON;
+const PackageJSON = @import("./package_json.zig").PackageJSON;
+usingnamespace @import("./data_url.zig");
+
+const StringBoolMap = std.StringHashMap(bool);
+
+const Path = fs.Path;
pub const SideEffectsData = struct {
source: *logger.Source,
range: logger.Range,
@@ -39,11 +47,13 @@ pub const Resolver = struct {
debug_logs: ?DebugLogs = null,
+ caches: cache.Cache.Set,
+
// These are sets that represent various conditions for the "exports" field
// in package.json.
- esm_conditions_default: std.StringHashMap(bool),
- esm_conditions_import: std.StringHashMap(bool),
- esm_conditions_require: std.StringHashMap(bool),
+ // esm_conditions_default: std.StringHashMap(bool),
+ // esm_conditions_import: std.StringHashMap(bool),
+ // esm_conditions_require: std.StringHashMap(bool),
// A special filtered import order for CSS "@import" imports.
//
@@ -88,6 +98,8 @@ pub const Resolver = struct {
indent: MutableString,
notes: std.ArrayList(logger.Data),
+ pub const FlushMode = enum { fail, success };
+
pub fn init(allocator: *std.mem.Allocator) DebugLogs {
return .{
.indent = MutableString.init(allocator, 0),
@@ -121,11 +133,15 @@ pub const Resolver = struct {
try d.notes.append(logger.rangeData(null, logger.Range.None, text));
}
+
+ pub fn addNoteFmt(d: *DebugLogs, comptime fmt: string, args: anytype) !void {
+ return try d.addNote(try std.fmt.allocPrint(d.notes.allocator, fmt, args));
+ }
};
pub const PathPair = struct {
- primary: logger.Path,
- secondary: ?logger.Path = null,
+ primary: Path,
+ secondary: ?Path = null,
};
pub const Result = struct {
@@ -133,18 +149,253 @@ pub const Resolver = struct {
jsx: options.JSX.Pragma = options.JSX.Pragma{},
- // plugin_data: void
+ is_external: bool = false,
+
+ different_case: ?fs.FileSystem.Entry.Lookup.DifferentCase = null,
+
+ // If present, any ES6 imports to this file can be considered to have no side
+ // effects. This means they should be removed if unused.
+ primary_side_effects_data: ?SideEffectsData = null,
+
+ // If true, the class field transform should use Object.defineProperty().
+ use_define_for_class_fields_ts: ?bool = null,
+
+ // If true, unused imports are retained in TypeScript code. This matches the
+ // behavior of the "importsNotUsedAsValues" field in "tsconfig.json" when the
+ // value is not "remove".
+ preserve_unused_imports_ts: bool = false,
+
+ // This is the "type" field from "package.json"
+ module_type: options.ModuleType,
+
+ debug_meta: ?DebugMeta = null,
+
+ pub const DebugMeta = struct {
+ notes: std.ArrayList(logger.Data),
+ suggestion_text: string = "",
+ suggestion_message: string = "",
+
+ pub fn init(allocator: *std.mem.Allocator) DebugMeta {
+ return DebugMeta{ .notes = std.ArrayList(logger.Data).init(allocator) };
+ }
+
+ pub fn logErrorMsg(m: *DebugMeta, log: *logger.Log, _source: ?*const logger.Source, r: logger.Range, comptime fmt: string, args: anytype) !void {
+ if (_source != null and m.suggestion_message.len > 0) {
+ const data = logger.rangeData(_source.?, r, m.suggestion_message);
+ data.location.?.suggestion = m.suggestion_text;
+ try m.notes.append(data);
+ }
+
+ try log.addMsg(Msg{
+ .kind = .err,
+ .data = logger.rangeData(_source, r, std.fmt.allocPrint(m.notes.allocator, fmt, args)),
+ .notes = m.toOwnedSlice(),
+ });
+ }
+ };
};
- pub fn resolve(r: *Resolver, source_dir: string, import_path: string, kind: ast.ImportKind) Result {}
+ pub fn isExternalPattern(r: *Resolver, import_path: string) bool {
+ Global.notimpl();
+ }
- fn dirInfoCached(r: *Resolver, path: string) !*DirInfo {
- // First, check the cache
- if (r.dir_cache.get(path)) |dir| {
- return dir;
+ pub fn flushDebugLogs(r: *Resolver, flush_mode: DebugLogs.FlushMode) !void {
+ if (r.debug_logs) |debug| {
+ defer {
+ debug.deinit();
+ r.debug_logs = null;
+ }
+
+ if (mode == .failure) {
+ try r.log.addRangeDebugWithNotes(null, .empty, debug.what, debug.notes.toOwnedSlice());
+ } else if (@enumToInt(r.log.level) <= @enumToInt(logger.Log.Level.verbose)) {
+ try r.log.addVerboseWithNotes(null, .empty, debug.what, debug.notes.toOwnedSlice());
+ }
+ }
+ }
+
+ pub fn resolve(r: *Resolver, source_dir: string, import_path: string, kind: ast.ImportKind) !?Result {
+ if (r.log.level == .verbose) {
+ if (r.debug_logs != null) {
+ r.debug_logs.?.deinit();
+ }
+
+ r.debug_logs = DebugLogs.init(r.allocator);
}
- const info = try r.dirInfoUncached(path);
+ // Certain types of URLs default to being external for convenience
+ if (r.isExternalPattern(import_path) or
+ // "fill: url(#filter);"
+ (kind.isFromCSS() and strings.startsWith(import_path, "#")) or
+
+ // "background: url(http://example.com/images/image.png);"
+ strings.startsWith(import_path, "http://") or
+
+ // "background: url(https://example.com/images/image.png);"
+ strings.startsWith(import_path, "https://") or
+
+ // "background: url(//example.com/images/image.png);"
+ strings.startsWith(import_path, "//"))
+ {
+ if (r.debug_logs) |debug| {
+ try debug.addNote("Marking this path as implicitly external");
+ }
+ r.flushDebugLogs(.success) catch {};
+ return Result{ .path_pair = PathPair{
+ .primary = Path{ .text = import_path },
+ .is_external = true,
+ } };
+ }
+
+ if (DataURL.parse(import_path) catch null) |_data_url| {
+ const data_url: DataURL = _data_url;
+ // "import 'data:text/javascript,console.log(123)';"
+ // "@import 'data:text/css,body{background:white}';"
+ if (data_url.decode_mime_type() != .Unsupported) {
+ if (r.debug_logs) |debug| {
+ debug.addNote("Putting this path in the \"dataurl\" namespace") catch {};
+ }
+ r.flushDebugLogs(.success) catch {};
+ return Resolver.Result{ .path_pair = PathPair{ .primary = Path{ .text = import_path, .namespace = "dataurl" } } };
+ }
+
+ // "background: url(data:image/png;base64,iVBORw0KGgo=);"
+ if (r.debug_logs) |debug| {
+ debug.addNote("Marking this \"dataurl\" as external") catch {};
+ }
+ r.flushDebugLogs(.success) catch {};
+ return Resolver.Result{
+ .path_pair = PathPair{ .primary = Path{ .text = import_path, .namespace = "dataurl" } },
+ .is_external = true,
+ };
+ }
+
+ // Fail now if there is no directory to resolve in. This can happen for
+ // virtual modules (e.g. stdin) if a resolve directory is not specified.
+ if (source_dir.len == 0) {
+ if (r.debug_logs) |debug| {
+ debug.addNote("Cannot resolve this path without a directory") catch {};
+ }
+ r.flushDebugLogs(.fail) catch {};
+ return null;
+ }
+
+ const hold = r.mutex.acquire();
+ defer hold.release();
+ }
+
+ pub fn resolveWithoutSymlinks(r: *Resolver, source_dir: string, import_path: string, kind: ast.ImportKind) !Result {
+ // This implements the module resolution algorithm from node.js, which is
+ // described here: https://nodejs.org/api/modules.html#modules_all_together
+ var result: Result = undefined;
+
+ // Return early if this is already an absolute path. In addition to asking
+ // the file system whether this is an absolute path, we also explicitly check
+ // whether it starts with a "/" and consider that an absolute path too. This
+ // is because relative paths can technically start with a "/" on Windows
+ // because it's not an absolute path on Windows. Then people might write code
+ // with imports that start with a "/" that works fine on Windows only to
+ // experience unexpected build failures later on other operating systems.
+ // Treating these paths as absolute paths on all platforms means Windows
+ // users will not be able to accidentally make use of these paths.
+ if (striongs.startsWith(import_path, "/") or std.fs.path.isAbsolutePosix(import_path)) {
+ if (r.debug_logs) |debug| {
+ debug.addNoteFmt("The import \"{s}\" is being treated as an absolute path", .{import_path}) catch {};
+ }
+
+ // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file
+ if (try r.dirInfoCached(source_dir)) |_dir_info| {
+ const dir_info: *DirInfo = _dir_info;
+ if (dir_info.ts_config_json) |tsconfig| {
+ if (tsconfig.paths.size() > 0) {}
+ }
+ }
+ }
+ }
+
+ pub const TSConfigExtender = struct {
+ visited: *StringBoolMap,
+ file_dir: string,
+ r: *Resolver,
+
+ pub fn extends(ctx: *TSConfigExtender, extends: String, range: logger.Range) ?*TSConfigJSON {
+ Global.notimpl();
+ // if (isPackagePath(extends)) {
+ // // // If this is a package path, try to resolve it to a "node_modules"
+ // // // folder. This doesn't use the normal node module resolution algorithm
+ // // // both because it's different (e.g. we don't want to match a directory)
+ // // // and because it would deadlock since we're currently in the middle of
+ // // // populating the directory info cache.
+ // // var current = ctx.file_dir;
+ // // while (true) {
+ // // // Skip "node_modules" folders
+ // // if (!strings.eql(std.fs.path.basename(current), "node_modules")) {
+ // // var paths1 = [_]string{ current, "node_modules", extends };
+ // // var join1 = std.fs.path.join(ctx.r.allocator, &paths1) catch unreachable;
+ // // const res = ctx.r.parseTSConfig(join1, ctx.visited) catch |err| {
+ // // if (err == error.ENOENT) {
+ // // continue;
+ // // } else if (err == error.ParseErrorImportCycle) {} else if (err != error.ParseErrorAlreadyLogged) {}
+ // // return null;
+ // // };
+ // // return res;
+
+ // // }
+ // // }
+ // }
+ }
+ };
+
+ pub fn parseTSConfig(r: *Resolver, file: string, visited: *StringBoolMap) !?*TSConfigJSON {
+ if (visited.contains(file)) {
+ return error.ParseErrorImportCycle;
+ }
+ visited.put(file, true) catch unreachable;
+ const entry = try r.caches.fs.readFile(r.fs, file);
+ const key_path = Path.init(file);
+
+ const source = logger.Source{
+ .key_path = key_path,
+ .pretty_path = r.prettyPath(key_path),
+ .contents = entry.contents,
+ };
+ const file_dir = std.fs.path.dirname(file);
+
+ var result = try TSConfigJSON.parse(r.allocator, r.log, r.opts, r.caches.json) orelse return null;
+
+ if (result.base_url) |base| {
+ // this might leak
+ if (!std.fs.path.isAbsolute(base)) {
+ var paths = [_]string{ file_dir, base };
+ result.base_url = std.fs.path.join(r.allocator, paths) catch unreachable;
+ }
+ }
+
+ if (result.paths.count() > 0 and (result.base_url_for_paths.len == 0 or !std.fs.path.isAbsolute(result.base_url_for_paths))) {
+ // this might leak
+ var paths = [_]string{ file_dir, base };
+ result.base_url_for_paths = std.fs.path.join(r.allocator, paths) catch unreachable;
+ }
+
+ return result;
+ }
+
+ // TODO:
+ pub fn prettyPath(r: *Resolver, path: Ptah) string {
+ return path.text;
+ }
+
+ pub fn parsePackageJSON(r: *Resolver, file: string) !?*PackageJSON {
+ return try PackageJSON.parse(r, file);
+ }
+
+ pub fn isPackagePath(path: string) bool {
+ // this could probably be flattened into something more optimized
+ return path[0] != '/' and !strings.startsWith(path, "./") and !strings.startsWith(path, "../") and !strings.eql(path, ".") and !strings.eql(path, "..");
+ }
+
+ fn dirInfoCached(r: *Resolver, path: string) !*DirInfo {
+ const info = r.dir_cache.get(path) orelse try r.dirInfoUncached(path);
try r.dir_cache.put(path, info);
}
@@ -215,5 +466,87 @@ pub const Resolver = struct {
}
// Propagate the browser scope into child directories
+ if (parent) |parent_info| {
+ info.enclosing_browser_scope = parent_info.enclosing_browser_scope;
+
+ // Make sure "absRealPath" is the real path of the directory (resolving any symlinks)
+ if (!r.opts.preserve_symlinks) {
+ if (parent_info.entries.get(base)) |entry| {
+ var symlink = entry.symlink(rfs);
+ if (symlink.len > 0) {
+ if (r.debug_logs) |logs| {
+ try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }));
+ }
+ info.abs_real_path = symlink;
+ } else if (parent_info.abs_real_path.len > 0) {
+ // this might leak a little i'm not sure
+ const parts = [_]string{ parent_info.abs_real_path, base };
+ symlink = std.fs.path.join(r.allocator, &parts);
+ if (r.debug_logs) |logs| {
+ try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved symlink \"{s}\" to \"{s}\"", .{ path, symlink }));
+ }
+ info.abs_real_path = symlink;
+ }
+ }
+ }
+ }
+
+ // Record if this directory has a package.json file
+ if (entries.get("package.json")) |entry| {
+ if (entry.kind(rfs) == .file) {
+ info.package_json = r.parsePackageJSON(path);
+
+ if (info.package_json) |pkg| {
+ if (pkg.browser_map != null) {
+ info.enclosing_browser_scope = info;
+ }
+
+ if (r.debug_logs) |logs| {
+ try logs.addNote(std.fmt.allocPrint(r.allocator, "Resolved package.json in \"{s}\"", .{
+ path,
+ }));
+ }
+ }
+ }
+ }
+
+ // Record if this directory has a tsconfig.json or jsconfig.json file
+ {
+ var tsconfig_path: ?string = null;
+ if (r.opts.tsconfig_override == null) {
+ var entry = entries.get("tsconfig.json");
+ if (entry.kind(rfs) == .file) {
+ const parts = [_]string{ path, "tsconfig.json" };
+ tsconfig_path = try std.fs.path.join(r.allocator, parts);
+ } else if (entries.get("jsconfig.json")) |jsconfig| {
+ if (jsconfig.kind(rfs) == .file) {
+ const parts = [_]string{ path, "jsconfig.json" };
+ tsconfig_path = try std.fs.path.join(r.allocator, parts);
+ }
+ }
+ } else if (parent == null) {
+ tsconfig_path = r.opts.tsconfig_override.?;
+ }
+
+ if (tsconfig_path) |tsconfigpath| {
+ var visited = std.StringHashMap(bool).init(r.allocator);
+ defer visited.deinit();
+ info.ts_config_json = r.parseTSConfig(tsconfigpath, visited) catch |err| {
+ const pretty = r.prettyPath(fs.Path{ .text = tsconfigpath, .namespace = "file" });
+
+ if (err == error.ENOENT) {
+ r.log.addErrorFmt(null, .empty, r.allocator, "Cannot find tsconfig file \"{s}\"", .{pretty});
+ } else if (err != error.ParseErrorAlreadyLogged) {
+ r.log.addErrorFmt(null, .empty, r.allocator, "Cannot read file \"{s}\": {s}", .{ pretty, @errorName(err) });
+ }
+ };
+ }
+ }
+
+ if (info.ts_config_json == null and parent != null) {
+ info.ts_config_json = parent.?.tsconfig_json;
+ }
+
+ return info;
}
};