aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/resolver/package_json.zig674
-rw-r--r--src/string_immutable.zig17
2 files changed, 657 insertions, 34 deletions
diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig
index 17d1b4494..056bf345d 100644
--- a/src/resolver/package_json.zig
+++ b/src/resolver/package_json.zig
@@ -9,6 +9,7 @@ const alloc = @import("../alloc.zig");
const fs = @import("../fs.zig");
const resolver = @import("./resolver.zig");
const js_lexer = @import("../js_lexer.zig");
+const resolve_path = @import("./resolve_path.zig");
// Assume they're not going to have hundreds of main fields or browser map
// so use an array-backed hash table instead of bucketed
const MainFieldMap = std.StringArrayHashMap(string);
@@ -627,39 +628,6 @@ pub const ExportsMap = struct {
root: Entry,
exports_range: logger.Range = logger.Range.None,
- pub const Status = enum {
- Undefined,
- UndefinedNoConditionsMatch, // A more friendly error message for when no conditions are matched
- Null,
- 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.
- InvalidModuleSpecifier,
-
- // 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.
- InvalidPackageTarget,
-
- // 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.
- ModuleNotFound,
-
- // The resolved path corresponds to a directory, which is not a supported target for module imports.
- UnsupportedDirectoryImport,
-
- pub inline fn isUndefined(this: Status) bool {
- return switch (this) {
- .Undefined, .UndefinedNoConditionsMatch => true,
- else => false,
- };
- }
- };
-
pub fn parse(allocator: *std.mem.Allocator, source: *const logger.Source, log: *logger.Log, json: js_ast.Expr) ?ExportsMap {
var visitor = Visitor{ .allocator = allocator, .source = source, .log = log };
@@ -760,7 +728,7 @@ pub const ExportsMap = struct {
}
}
- // this leaks, but it's fine.
+ // this leaks a lil, but it's fine.
expansion_keys = expansion_keys[0..expansion_key_i];
// Let expansion_keys be the list of keys of matchObj ending in "/" or "*",
@@ -834,6 +802,10 @@ pub const ExportsMap = struct {
};
};
+ pub fn keysStartWithDot(this: *const Entry) bool {
+ return this.data == .map and this.data.map.list.len > 0 and strings.startsWithChar(this.data.map.list.items(.key), '.');
+ }
+
pub fn valueForKey(this: *const Entry, key_: string) ?Entry {
switch (this.data) {
.map => |map| {
@@ -854,3 +826,637 @@ pub const ExportsMap = struct {
}
};
};
+
+pub const ESModule = struct {
+ pub const ConditionsMap = std.AutoArrayHashMap(string, void);
+
+ debug_logs: ?*resolver.DebugLogs = null,
+ conditions: ConditionsMap,
+ allocator: *std.mem.Allocator,
+
+ pub const Resolution = struct {
+ status: Status = Status.Undefined,
+ path: string = "",
+ debug: Debug = Debug{},
+
+ pub const Debug = struct {
+ // This is the range of the token to use for error messages
+ token: logger.Range = logger.Range.None,
+ // If the status is "UndefinedNoConditionsMatch", this is the set of
+ // conditions that didn't match. This information is used for error messages.
+ unmatched_conditions: []string = &[_]string{},
+ };
+ };
+
+ pub const Status = enum {
+ Undefined,
+ UndefinedNoConditionsMatch, // A more friendly error message for when no conditions are matched
+ Null,
+ 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.
+ InvalidModuleSpecifier,
+
+ // 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.
+ InvalidPackageTarget,
+
+ // 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.
+ ModuleNotFound,
+
+ // The resolved path corresponds to a directory, which is not a supported target for module imports.
+ UnsupportedDirectoryImport,
+
+ pub inline fn isUndefined(this: Status) bool {
+ return switch (this) {
+ .Undefined, .UndefinedNoConditionsMatch => true,
+ else => false,
+ };
+ }
+ };
+
+ pub const Package = struct {
+ name: string,
+ subpath: string,
+
+ pub fn parse(specifier: string, subpath_buf: []u8) ?Package {
+ if (specifier.len == 0) return null;
+ var package = Package{ .name = "", .subpath = "" };
+
+ var slash = strings.indexOfCharNeg(specifier, '/');
+ if (!string.startsWithChar(specifier, '@')) {
+ slash = if (slash == -1) specifier.len else slash;
+ package.name = specifier[0..@intCast(usize, slash)];
+ } else {
+ if (slash == -1) return null;
+
+ const slash2 = strings.indexOfChar(specifier[@intCast(usize, slash) + 1 ..]) orelse
+ specifier[@intCast(u32, slash + 1)..].len;
+ package.name = specifier[0 .. @intCast(usize, slash + 1) + slash2];
+ }
+
+ if (strings.startsWith(package.name, ".") or strings.containsAny(package.name, "\\%"))
+ return;
+
+ std.mem.copy(u8, subpath_buf[1..], package.subpath);
+ subpath_buf[0] = '.';
+ package.subpath = subpath_buf[0 .. package.subpath.len + 1];
+ return package;
+ }
+ };
+
+ const ReverseKind = enum { exact, pattern, prefix };
+ pub const ReverseResolution = struct {
+ subpath: string = "",
+ token: logger.Range = logger.Range.None,
+ };
+ const invalid_percent_chars = [_]string{
+ "%2f",
+ "%2F",
+ "%5c",
+ "%5C",
+ };
+
+ threadlocal var resolved_path_buf_percent: [std.fs.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);
+
+ if (result.status != .Exact and result.status != .Inexact) {
+ return result;
+ }
+
+ // If resolved contains any percent encodings of "/" or "\" ("%2f" and "%5C"
+ // respectively), then throw an Invalid Module Specifier error.
+ const PercentEncoding = @import("../query_string_map.zig").PercentEncoding;
+ var fbs = std.io.fixedBufferStream(&resolved_path_buf_percent);
+ var writer = fbs.writer();
+ const len = PercentEncoding.decode(@TypeOf(&writer), &writer, result.path) catch return Resolution{ .status = .InvalidModuleSpecifier, .path = result.path, .debug = result.debug };
+ const resolved_path = resolved_path_buf_percent[0..len];
+
+ var found: string = "";
+ if (strings.contains(resolved_path, invalid_percent_chars[0])) {
+ found = invalid_percent_chars[0];
+ } else if (strings.contains(resolved_path, invalid_percent_chars[1])) {
+ found = invalid_percent_chars[1];
+ } else if (strings.contains(resolved_path, invalid_percent_chars[2])) {
+ found = invalid_percent_chars[2];
+ } else if (strings.contains(resolved_path, invalid_percent_chars[3])) {
+ found = invalid_percent_chars[3];
+ }
+
+ if (found.len != 0) {
+ return Resolution{ .status = .InvalidModuleSpecifier, .path = result.path, .debug = result.debug };
+ }
+
+ // If resolved is a directory, throw an Unsupported Directory Import error.
+ if (strings.endsWithAnyComptime(resolved_path, "/\\")) {
+ return Resolution{ .status = .UnsupportedDirectoryImport, .path = result.path, .debug = result.debug };
+ }
+
+ result.path = resolved_path;
+ return result;
+ }
+
+ fn resolveExports(
+ r: *const ESModule,
+ package_url: string,
+ subpath: string,
+ exports: ExportsMap.Entry,
+ ) Resolution {
+ if (exports.kind == .invalid) {
+ if (r.debug_logs) |logs| {
+ logs.addNote("Invalid package configuration") catch unreachable;
+ }
+
+ return Resolution{ .status = .InvalidPackageConfiguration, .debug = .{ .token = exports.first_token } };
+ }
+
+ if (strings.eqlComptime(subpath, ".")) {
+ var main_export = ExportsMap.Entry{ .data = .{ .null = void{} } };
+ if (switch (exports.data) {
+ .string,
+ .array,
+ => true,
+ .array => !exports.keysStartWithDot(),
+ }) {
+ main_export = exports;
+ } else if (exports.data == .map) {
+ if (exports.valueForKey(".")) |value| {
+ main_export = value;
+ }
+ }
+
+ if (main_export.data != .null) {
+ if (r.resolveTarget(package_url, main_export, "", false)) |result| {
+ if (result.status != .Null and result.status != .Undefined) {
+ return result;
+ }
+ }
+ }
+ } else if (exports.data == .map and exports.keysStartWithDot()) {
+ if (r.resolveImportsExports(subpath, exports, package_url)) |result| {
+ if (result.status != .Null and result.status != .Undefined) {
+ return result;
+ }
+ }
+ }
+
+ if (r.debug_logs) |logs| {
+ logs.addNoteFmt("The path \"{s}\" was not exported", .{subpath}) catch unreachable;
+ }
+
+ return Resolution{ .status = .PackagePathNotExported, .debug = .{ .token = exports.first_token } };
+ }
+
+ fn resolveImportsExports(
+ r: *const ESModule,
+ match_key: string,
+ match_obj: ExportsMap.Entry,
+ package_url: string,
+ ) Resolution {
+ if (r.debug_logs) |logs| {
+ logs.addNoteFmt("Checking object path map for \"{s}\"", .{match_key}) catch unreachable;
+ }
+
+ if (!strings.endsWithChar(match_key, '.')) {
+ if (match_obj.valueForKey(match_Key)) |target| {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("Found \"{s}\"", .{match_key}) catch unreachable;
+ }
+
+ return r.resolveTarget(package_url, target, "", false);
+ }
+ }
+
+ if (match_obj.data == .map) {
+ const expansion_keys = match_obj.data.map.expansion_keys;
+ for (expansion_keys) |expansion| {
+ // If expansionKey ends in "*" and matchKey starts with but is not equal to
+ // the substring of expansionKey excluding the last "*" character
+ if (strings.endsWithChar(expansion.key, '*')) {
+ const substr = expansion.key[0 .. expansion.key.len - 1];
+ if (strings.startsWith(match_key, substr) and !strings.eql(match_key, substr)) {
+ const target = expansion.value;
+ const subpath = match_key[expansion.key.len - 1 ..];
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath }) catch unreachable;
+ }
+
+ return r.resolveTarget(package_url, target, subpath, true);
+ }
+ }
+
+ if (strings.startsWith(match_key, expansion.key)) {
+ const target = expansion.value;
+ const subpath = match_key[expansion.key.len..];
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The key \"{s}\" matched with \"{s}\" left over", .{ expansion.key, subpath }) catch unreachable;
+ }
+
+ var result = r.resolveTarget(package_url, target, subpath, false);
+ result.status = if (result.status == .Exact)
+ // Return the object { resolved, exact: false }.
+ .Inexact
+ else
+ result.status;
+
+ return result;
+ }
+
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The key \"{s}\" did not match", .{expansion.key}) catch unreachable;
+ }
+ }
+ }
+
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("No keys matched \"{s}\"", .{expansion.key}) catch unreachable;
+ }
+
+ return Resolution{
+ .status = .Null,
+ .debug = .{ .token = match_obj.first_token },
+ };
+ }
+
+ threadlocal var resolve_target_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+ fn resolveTarget(
+ r: *const ESModule,
+ package_url: string,
+ target: ExportsMap.Entry,
+ subpath: string,
+ comptime pattern: bool,
+ ) Resolution {
+ switch (target.data) {
+ .string => |str| {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("Checking path \"{s}\" against target \"{s}\"", .{ subpath, str }) catch unreachable;
+ log.increaseIndent() catch unreachable;
+ }
+ defer {
+ if (r.debug_logs) |log| {
+ log.decreaseIndent() catch unreachable;
+ }
+ }
+
+ // If pattern is false, subpath has non-zero length and target
+ // does not end with "/", throw an Invalid Module Specifier error.
+ if (comptime !pattern) {
+ if (subpath.len > 0 and !strings.endsWithChar(str, '/')) {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The target \"{s}\" is invalid because it doesn't end with a \"/\"") catch unreachable;
+ }
+
+ return Resolution{ .path = str, .status = .InvalidModuleSpecifier, .debug = .{ .token = target.first_token } };
+ }
+ }
+
+ if (!strings.startsWith(string, "./")) {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The target \"{s}\" is invalid because it doesn't start with a \"./\"") catch unreachable;
+ }
+
+ return Resolution{ .path = str, .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
+ }
+
+ // If target split on "/" or "\" contains any ".", ".." or "node_modules"
+ // segments after the first segment, throw an Invalid Package Target error.
+ if (findInvalidSegment(str)) |invalid| {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The target \"{s}\" is invalid because it contains an invalid segment \"{s}\"", .{ str, invalid }) catch unreachable;
+ }
+
+ return Resolution{ .path = str, .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
+ }
+
+ // Let resolvedTarget be the URL resolution of the concatenation of packageURL and target.
+ var parts = [_]string{ package_url, str };
+ const resolved_target = resolve_path.joinStringBuf(&resolve_target_buf, parts, .auto);
+
+ // If target split on "/" or "\" contains any ".", ".." or "node_modules"
+ // segments after the first segment, throw an Invalid Package Target error.
+ if (findInvalidSegment(resolved_target)) |invalid| {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The target \"{s}\" is invalid because it contains an invalid segment \"{s}\"", .{ str, invalid }) catch unreachable;
+ }
+
+ return Resolution{ .path = str, .status = .InvalidModuleSpecifier, .debug = .{ .token = target.first_token } };
+ }
+
+ if (comptime pattern) {
+ // Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath.
+ const len = std.mem.replacementSize(u8, resolved_target, "*", subpath);
+ _ = std.mem.replace(u8, resolved_target, "*", subpath, &resolve_target_buf);
+ const result = resolve_target_buf[0..len];
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("Subsituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, resolved_target, result }) catch unreachable;
+ }
+
+ return Resolution{ .path = result, .status = .Exact, .debug = .{ .token = target.first_token } };
+ } else {
+ var parts2 = [_]string{ package_url, str, subpath };
+ const result = resolve_path.joinStringBuf(&resolve_target_buf, parts2, .auto);
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("Subsituted \"{s}\" for \"*\" in \".{s}\" to get \".{s}\" ", .{ subpath, resolved_target, result }) catch unreachable;
+ }
+
+ return Resolution{ .path = result, .status = .Exact, .debug = .{ .token = target.first_token } };
+ }
+ },
+ .map => |object| {
+ var did_find_map_entry = false;
+ var last_map_entry: ExportsMap.Entry.Data.Map.MapEntry = undefined;
+
+ const slice = object.list.slice();
+ const keys = slice.items(.key);
+ for (keys) |key, i| {
+ if (strings.eqlComptime(key, "default") or r.conditions.contains(key)) {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The key \"{s}\" matched", .{key}) catch unreachable;
+ }
+
+ var result = r.resolveTarget(package_url, target, subpath, pattern);
+ if (result.status.isUndefined()) {
+ did_find_map_entry = true;
+ last_map_entry = slice.get(i);
+ continue;
+ }
+
+ return result;
+ }
+
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The key \"{s}\" did not match", .{key}) catch unreachable;
+ }
+ }
+
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("No keys matched") catch unreachable;
+ }
+
+ var return_target = target;
+ // ALGORITHM DEVIATION: Provide a friendly error message if no conditions matched
+ if (keys.len > 0 and !target.keysStartWithDot()) {
+ if (did_find_map_entry and
+ last_map_entry.value.data == .map and
+ last_map_entry.value.data.map.list.items.len > 0 and
+ !last_map_entry.value.keysStartWithDot())
+ {
+ // If a top-level condition did match but no sub-condition matched,
+ // complain about the sub-condition instead of the top-level condition.
+ // This leads to a less confusing error message. For example:
+ //
+ // "exports": {
+ // "node": {
+ // "require": "./dist/bwip-js-node.js"
+ // }
+ // },
+ //
+ // We want the warning to say this:
+ //
+ // note: None of the conditions provided ("require") match any of the
+ // currently active conditions ("default", "import", "node")
+ // 14 | "node": {
+ // | ^
+ //
+ // We don't want the warning to say this:
+ //
+ // note: None of the conditions provided ("browser", "electron", "node")
+ // match any of the currently active conditions ("default", "import", "node")
+ // 7 | "exports": {
+ // | ^
+ //
+ // More information: https://github.com/evanw/esbuild/issues/1484
+ return_target = last_map_entry.value;
+ }
+
+ return Resolution{
+ .path = "",
+ .status = .UndefinedNoConditionsMatch,
+ .debug = .{
+ .token = target.first_token,
+ .unmatched_conditions = return_target.data.map.list.items(.keys),
+ },
+ };
+ }
+
+ return Resolution{
+ .path = "",
+ .status = .UndefinedNoConditionsMatch,
+ .debug = .{ .token = target.first_token },
+ };
+ },
+ .array => |array| {
+ if (array.len == 0) {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The path \"{s}\" is an empty array", .{subpath}) catch unreachable;
+ }
+
+ return Resolution{ .path = "", .status = .Null, .debug = .{ .token = target.first_token } };
+ }
+
+ var last_exception = Status.Undefined;
+ var last_debug = Resolution.Debug{ .token = target.first_token };
+
+ for (array) |targetValue, i| {
+ // Let resolved be the result, continuing the loop on any Invalid Package Target error.
+ const result = r.resolveTarget(package_url, targetValue, subpath, pattern);
+ if (result.status == .InvalidPackageTarget or result.status == .Null) {
+ last_debug = result.debug;
+ last_exception = result.status;
+ }
+
+ if (result.status.isUndefined()) {
+ continue;
+ }
+
+ return result;
+ }
+
+ return Resolution{ .path = "", .status = last_exception, .debug = last_debug };
+ },
+ .null => {
+ if (r.debug_logs) |log| {
+ log.addNoteFmt("The path \"{s}\" is null", .{subpath}) catch unreachable;
+ }
+
+ return Resolution{ .path = "", .status = .Null, .debug = .{ .token = target.first_token } };
+ },
+ else => {},
+ }
+
+ if (r.debug_logs) |logs| {
+ logs.addNoteFmt("Invalid package target for path \"{s}\"", .{subpath}) catch unreachable;
+ }
+
+ return Resolution{ .status = .InvalidPackageTarget, .debug = .{ .token = target.first_token } };
+ }
+
+ fn resolveExportsReverse(
+ r: *const ESModule,
+ query: string,
+ root: ExportsMap.Entry,
+ ) ?ReverseResolution {
+ if (root.data == .map and root.keysStartWithDot()) {
+ if (r.resolveImportsExportsReverse(query, root)) |res| {
+ return res;
+ }
+ }
+
+ return null;
+ }
+
+ fn resolveImportsExportsReverse(
+ r: *const ESModule,
+ query: string,
+ match_obj: ExportsMap.Entry,
+ ) ?ReverseResolution {
+ if (match_obj.data != .map) return null;
+ const map = match_obj.data.map;
+
+ if (!strings.endsWithChar(query, "*")) {
+ var slices = map.list.slice();
+ var keys = slices.items(.key);
+ var values = slices.items(.value);
+ for (keys) |key, i| {
+ if (r.resolveTargetReverse(query, key, values[i], .exact)) |result| {
+ return result;
+ }
+ }
+ }
+
+ for (map.expansion_keys) |expansion| {
+ if (strings.endsWithChar(expansion.key, '*')) {
+ if (r.resolveTargetReverse(query, expansion.key, expansion.value, .pattern)) |result| {
+ return result;
+ }
+ }
+
+ if (r.resolveTargetReverse(query, expansion.key, expansion.value, .reverse)) |result| {
+ return result;
+ }
+ }
+ }
+
+ threadlocal var resolve_target_reverse_prefix_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+ threadlocal var resolve_target_reverse_prefix_buf2: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+
+ fn resolveTargetReverse(
+ r: *const ESModule,
+ query: string,
+ key: string,
+ target: ExportsMap.Entry,
+ comptime kind: ReverseKind,
+ ) ?ReverseResolution {
+ switch (target.data) {
+ .string => |str| {
+ switch (comptime kind) {
+ .exact => {
+ if (strings.eql(query, str)) {
+ return ReverseResolution{ .subpath = str, .token = target.first_token };
+ }
+ },
+ .prefix => {
+ if (strings.startsWith(query, str)) {
+ return ReverseResolution{
+ .subpath = std.fmt.bufPrint(&resolve_target_reverse_prefix_buf, "{s}{s}", .{ key, query[str.len..] }) catch unreachable,
+ .token = target.first_token,
+ };
+ }
+ },
+ .pattern => {
+ const key_without_trailing_star = std.mem.trimRight(u8, key, "*");
+
+ const star = strings.indexOfChar(str, '*') orelse {
+ // Handle the case of no "*"
+ if (strings.eql(query, str)) {
+ return ReverseResolution{ .subpath = key_without_trailing_star, .token = target.first_token };
+ }
+ return null;
+ };
+
+ // Only support tracing through a single "*"
+ const prefix = str[0..star];
+ const suffix = str[star + 1 ..];
+ if (strings.startsWith(query, prefix) and !strings.containsChar(suffix, '*')) {
+ const after_prefix = query[prefix.len..];
+ if (strings.endsWith(after_prefix, suffix)) {
+ const star_data = after_prefix[0 .. after_prefix.len - suffix.len];
+ return ReverseResolution{
+ .subpath = std.fmt.bufPrint(
+ &resolve_target_reverse_prefix_buf2,
+ "{s}{s}",
+ .{
+ key_without_trailing_star,
+ star_data,
+ },
+ ) catch unreachable,
+ .token = target.first_token,
+ };
+ }
+ }
+ },
+ }
+ },
+ .map => |map| {
+ const slice = map.list.slice();
+ const keys = slice.items(.key);
+ for (keys) |map_key, i| {
+ if (strings.eqlComptime(map_key, "default") or r.conditions.contains(map_key)) {
+ if (r.resolveTargetReverse(query, key, slice.items(.value)[i], kind)) |result| {
+ return result;
+ }
+ }
+ }
+ },
+
+ .array => |array| {
+ for (array) |target_value| {
+ if (r.resolveTargetReverse(query, key, target_value, kind)) |result| {
+ return result;
+ }
+ }
+ },
+
+ else => {},
+ }
+
+ return null;
+ }
+};
+
+fn findInvalidSegment(path_: string) ?string {
+ var slash = strings.indexAnyComptime(path_, "/\\") orelse return "";
+ var path = path_[slash + 1 ..];
+
+ while (path.len > 0) {
+ var segment = path;
+ if (strings.indexAnyComptime(path, "/\\")) |new_slash| {
+ segment = path[0..new_slash];
+ path = path[new_slash + 1 ..];
+ } else {
+ path = "";
+ }
+
+ switch (segment.len) {
+ 1 => {
+ if (strings.eqlComptimeIgnoreLen(segment, ".")) return segment;
+ },
+ 2 => {
+ if (strings.eqlComptimeIgnoreLen(segment, "..")) return segment;
+ },
+ "node_modules".len => {
+ if (strings.eqlComptimeIgnoreLen(segment, "node_modules")) return segment;
+ },
+ else => {},
+ }
+ }
+
+ return null;
+}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index 7010cd538..4bfc707b7 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -23,10 +23,27 @@ pub inline fn indexAny(in: anytype, target: string) ?usize {
return null;
}
+pub inline fn indexAnyComptime(target: string, comptime chars: string) ?usize {
+ for (target) |parent, i| {
+ inline for (chars) |char| {
+ if (char == parent) return i;
+ }
+ }
+ return null;
+}
+
pub inline fn indexOfChar(self: string, char: u8) ?usize {
return std.mem.indexOfScalar(@TypeOf(char), self, char);
}
+pub fn indexOfCharNeg(self: string, char: u8) i32 {
+ var i: u32 = 0;
+ while (i < self.len) : (i += 1) {
+ if (self[i] == value) return @intCast(i32, i);
+ }
+ return -1;
+}
+
pub inline fn lastIndexOfChar(self: string, char: u8) ?usize {
return std.mem.lastIndexOfScalar(u8, self, char);
}