diff options
-rw-r--r-- | src/api/schema.ts | 166 | ||||
-rw-r--r-- | src/bundler.zig | 1 | ||||
-rw-r--r-- | src/cache.zig | 9 | ||||
-rw-r--r-- | src/fs.zig | 496 | ||||
-rw-r--r-- | src/global.zig | 15 | ||||
-rw-r--r-- | src/js_ast.zig | 29 | ||||
-rw-r--r-- | src/options.zig | 56 | ||||
-rw-r--r-- | src/resolver/resolve_path.zig | 82 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 219 | ||||
-rw-r--r-- | src/string_mutable.zig | 4 | ||||
-rw-r--r-- | test.js | 57 | ||||
-rw-r--r-- | test.jsx | 27 |
12 files changed, 892 insertions, 269 deletions
diff --git a/src/api/schema.ts b/src/api/schema.ts deleted file mode 100644 index 344e87604..000000000 --- a/src/api/schema.ts +++ /dev/null @@ -1,166 +0,0 @@ -import type {ByteBuffer} from "peechy"; - -type byte = number; -type float = number; -type int = number; -type alphanumeric = string; -type uint = number; -type int8 = number; -type lowp = number; -type int16 = number; -type int32 = number; -type float32 = number; -type uint16 = number; -type uint32 = number; - export enum Loader { - jsx = 1, - js = 2, - ts = 3, - tsx = 4, - css = 5, - file = 6, - json = 7 - } - export const LoaderKeys = { - 1: "jsx", - jsx: "jsx", - 2: "js", - js: "js", - 3: "ts", - ts: "ts", - 4: "tsx", - tsx: "tsx", - 5: "css", - css: "css", - 6: "file", - file: "file", - 7: "json", - json: "json" - } - export enum JSXRuntime { - automatic = 1, - classic = 2 - } - export const JSXRuntimeKeys = { - 1: "automatic", - automatic: "automatic", - 2: "classic", - classic: "classic" - } - export enum TransformResponseStatus { - success = 1, - fail = 2 - } - export const TransformResponseStatusKeys = { - 1: "success", - success: "success", - 2: "fail", - fail: "fail" - } - export enum MessageKind { - err = 1, - warn = 2, - note = 3, - debug = 4 - } - export const MessageKindKeys = { - 1: "err", - err: "err", - 2: "warn", - warn: "warn", - 3: "note", - note: "note", - 4: "debug", - debug: "debug" - } - export interface JSX { - factory: string; - runtime: JSXRuntime; - fragment: string; - production: boolean; - import_source: string; - react_fast_refresh: boolean; - loader_keys: string[]; - loader_values: Loader[]; - } - - export interface TransformOptions { - jsx: JSX; - ts: boolean; - base_path: string; - define_keys: string[]; - define_values: string[]; - } - - export interface FileHandle { - path: string; - size: uint; - fd: uint; - } - - export interface Transform { - handle?: FileHandle; - path?: string; - contents?: Uint8Array; - loader?: Loader; - options?: TransformOptions; - } - - export interface OutputFile { - data: Uint8Array; - path: string; - } - - export interface TransformResponse { - status: TransformResponseStatus; - files: OutputFile[]; - errors: Message[]; - } - - export interface Location { - file: string; - namespace: string; - line: int32; - column: int32; - line_text: string; - suggestion: string; - offset: uint; - } - - export interface MessageData { - text?: string; - location?: Location; - } - - export interface Message { - kind: MessageKind; - data: MessageData; - notes: MessageData[]; - } - - export interface Log { - warnings: uint32; - errors: uint32; - msgs: Message[]; - } - - export declare function encodeJSX(message: JSX, bb: ByteBuffer): void; - export declare function decodeJSX(buffer: ByteBuffer): JSX; - export declare function encodeTransformOptions(message: TransformOptions, bb: ByteBuffer): void; - export declare function decodeTransformOptions(buffer: ByteBuffer): TransformOptions; - export declare function encodeFileHandle(message: FileHandle, bb: ByteBuffer): void; - export declare function decodeFileHandle(buffer: ByteBuffer): FileHandle; - export declare function encodeTransform(message: Transform, bb: ByteBuffer): void; - export declare function decodeTransform(buffer: ByteBuffer): Transform; - export declare function encodeOutputFile(message: OutputFile, bb: ByteBuffer): void; - export declare function decodeOutputFile(buffer: ByteBuffer): OutputFile; - export declare function encodeTransformResponse(message: TransformResponse, bb: ByteBuffer): void; - export declare function decodeTransformResponse(buffer: ByteBuffer): TransformResponse; - export declare function encodeLocation(message: Location, bb: ByteBuffer): void; - export declare function decodeLocation(buffer: ByteBuffer): Location; - export declare function encodeMessageData(message: MessageData, bb: ByteBuffer): void; - export declare function decodeMessageData(buffer: ByteBuffer): MessageData; - export declare function encodeMessage(message: Message, bb: ByteBuffer): void; - export declare function decodeMessage(buffer: ByteBuffer): Message; - export declare function encodeLog(message: Log, bb: ByteBuffer): void; - export declare function decodeLog(buffer: ByteBuffer): Log; diff --git a/src/bundler.zig b/src/bundler.zig index 62d663c7c..6f6e58fcc 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -11,7 +11,6 @@ pub const Bundler = struct { pub fn init(options: options.TransformOptions, allocator: *std.mem.Allocator) Bundler { var log = logger.Log.init(allocator); - return Bundler{ .options = options, .allocator = allocator, diff --git a/src/cache.zig b/src/cache.zig new file mode 100644 index 000000000..1f4abb91c --- /dev/null +++ b/src/cache.zig @@ -0,0 +1,9 @@ +pub const Cache = struct { + pub const Fs = struct {}; + + pub const Css = struct {}; + + pub const JavaScript = struct {}; + + pub const Json = struct {}; +}; diff --git a/src/fs.zig b/src/fs.zig index c765208e2..4952e3c18 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -4,6 +4,7 @@ usingnamespace @import("global.zig"); const alloc = @import("alloc.zig"); const expect = std.testing.expect; +const Mutex = std.Thread.Mutex; // pub const FilesystemImplementation = @import("fs_impl.zig"); @@ -16,13 +17,134 @@ pub const Stat = packed struct { kind: FileSystemEntry.Kind, }; +threadlocal var scratch_lookup_buffer = [_]u8{0} ** 255; + pub const FileSystem = struct { // This maps paths relative to absolute_working_dir to the structure of arrays of paths stats: std.StringHashMap(Stat) = undefined, - entries: std.ArrayList(FileSystemEntry), + allocator: *std.mem.Allocator, + top_level_dir = "/", + fs: Implementation, + + pub const Error = error{ + ENOENT, + EACCESS, + INVALID_NAME, + ENOTDIR, + }; + + pub const DirEntry = struct { + pub const EntryMap = std.StringArrayHashMap(*Entry); + dir: string, + data: EntryMap, + + pub fn empty(dir: string, allocator: std.mem.Allocator) DirEntry { + return DirEntry{ .dir = dir, .data = EntryMap.init(allocator) }; + } + + pub fn init(dir: string, allocator: std.mem.Allocator) DirEntry { + return DirEntry{ .dir = dir, .data = EntryMap.init(allocator) }; + } + + pub const Err = struct { + original_error: anyerror, + canonical_error: anyerror, + }; + + pub fn init(dir: string, allocator: *std.mem.Allocator) DirEntry { + return DirEntry{ + .dir = dir, + .data = std.StringArrayHashMap(*Entry).init(allocator), + }; + } + + pub fn deinit(d: *DirEntry) void { + d.data.allocator.free(d.dir); + + for (d.data.items()) |item| { + item.value.deinit(d.data.allocator); + } + d.data.deinit(); + } + + pub fn get(entry: *DirEntry, _query: string) ?Entry.Lookup { + if (_query.len == 0) return null; + + var end: usize = 0; + std.debug.assert(scratch_lookup_buffer.len >= _query.len); + for (_query) |c, i| { + scratch_lookup_buffer[i] = std.ascii.toLower(c); + end = i; + } + const query = scratch_lookup_buffer[0 .. end + 1]; + const result = entry.data.get(query) orelse return null; + if (!strings.eql(dir.base, query)) { + return Entry.Lookup{ .entry = result, .different_case = Entry.Lookup.DifferentCase{ + .dir = entry.dir, + .query = _query, + .actual = result.base, + } }; + } + + return Entry.Lookup{ .entry = entry }; + } + }; + + pub const Entry = struct { + cache: Cache = Cache{}, + dir: string, + base: string, + mutex: Mutex, + need_stat: bool = true, + + pub const Lookup = struct { + entry: *Entry, + different_case: ?DifferentCase, + + pub const DifferentCase = struct { + dir: string, + query: string, + actual: string, + }; + }; - absolute_working_dir = "/", - implementation: anytype = undefined, + pub fn deinit(e: *Entry, allocator: *std.mem.Allocator) void { + allocator.free(e.base); + allocator.free(e.dir); + allocator.free(e.cache.kind); + allocator.destroy(e); + } + + pub const Cache = struct { + symlink: string = "", + kind: Kind, + }; + + pub const Kind = enum { + dir, + file, + }; + + pub fn kind(entry: *Entry, fs: *Implementation) Kind { + const held = entry.mutex.acquire(); + defer held.release(); + if (entry.need_stat) { + entry.need_stat = false; + entry.cache = fs.kind(entry.dir, entry.base); + } + return entry.cache.kind; + } + + pub fn symlink(entry: *Entry, fs: *Implementation) string { + const held = entry.mutex.acquire(); + defer held.release(); + if (entry.need_stat) { + entry.need_stat = false; + entry.cache = fs.kind(entry.dir, entry.base); + } + return entry.cache.symlink; + } + }; // pub fn statBatch(fs: *FileSystemEntry, paths: []string) ![]?Stat { @@ -37,30 +159,365 @@ pub const FileSystem = struct { // } - pub fn Implementation(comptime Context: type) type { - return struct { - context: *Context, + pub const RealFS = struct { + entries_mutex: Mutex = Mutex{}, + entries: std.StringHashMap(EntriesOption), + allocator: *std.mem.Allocator, + do_not_cache_entries: bool = false, + limiter: Limiter, + watcher: ?std.StringHashMap(WatchData) = null, + watcher_mutex: Mutex = Mutex{}, + + pub const ModKey = struct { + inode: std.fs.File.INode = 0, + size: u64 = 0, + mtime: i128 = 0, + mode: std.fs.File.Mode = 0, + + pub const Error = error{ + Unusable, + }; + pub fn generate(fs: *RealFS, path: string) anyerror!ModKey { + var file = try std.fs.openFileAbsolute(path, std.fs.File.OpenFlags{ .read = true }); + defer file.close(); + const stat = try file.stat(); + + const seconds = stat.mtime / std.time.ns_per_s; + + // We can't detect changes if the file system zeros out the modification time + if (seconds == 0 and std.time.ns_per_s == 0) { + return Error.Unusable; + } + + // Don't generate a modification key if the file is too new + const now = std.time.nanoTimestamp(); + const now_seconds = now / std.time.ns_per_s; + if (seconds > seconds or (seconds == now_seconds and stat.mtime > now)) { + return Error.Unusable; + } + + return ModKey{ + .inode = stat.inode, + .size = stat.size, + .mtime = stat.mtime, + .mode = stat.mode, + // .uid = stat. + }; + } + pub const SafetyGap = 3; + }; + + fn modKeyError(fs: *RealFS, path: string, err: anyerror) !void { + if (fs.watcher) |watcher| { + const hold = watch_data.watch_mutex.acquire(); + defer hold.release(); + var state = WatchData.State.file_missing; + + switch (err) { + ModKey.Error.Unusable => { + state = WatchData.State.file_unusable_mod_key; + }, + else => {}, + } + + var entry = try watcher.getOrPutValue(path, WatchData{ .state = state }); + entry.value.state = state; + } + return err; + } + + pub fn modKey(fs: *RealFS, path: string) !ModKey { + fs.limiter.before(); + defer fs.limiter.after(); - pub fn statBatch(context: *Context, path: string) ![]?Stat { - return try context.statBatch(path); + const key = ModKey.generate(fs, path) catch |err| return fs.modKeyError(path, err); + if (fs.watcher) |watcher| { + const hold = fs.watcher_mutex.acquire(); + defer hold.release(); + + var entry = try watcher.getOrPutValue(path, WatchData{ .state = .file_has_mod_key, .mod_key = key }); + entry.value.mod_key = key; } - pub fn stat(context: *Context, path: string) !?Stat { - return try context.stat(path); + return key; + } + + pub const WatchData = struct { + dir_entries: []string = &([_]string{}), + file_contents: string = "", + mod_key: ModKey = ModKey{}, + watch_mutex: Mutex = Mutex{}, + state: State = State.none, + + pub const State = enum { + none, + dir_has_entries, + dir_missing, + file_has_mod_key, + file_need_mod_key, + file_missing, + file_unusable_mod_key, + }; + }; + + pub const EntriesOption = union(Tag) { + entries: DirEntry, + err: DirEntry.Err, + + pub const Tag = enum { + entries, + err, + }; + }; + + // Limit the number of files open simultaneously to avoid ulimit issues + pub const Limiter = struct { + chan: std.event.Channel(bool), + + pub fn init(allocator: *std.mem.Allocator) !Limiter { + var limiter = Limiter{ .chan = std.event.Channel(bool) }; + var buf = try allocator.create(bool, 32); + limiter.chan.init(buf); + + return limiter; } - pub fn readFile(context: *Context, path: string) !?File { - return try context.readFile(path); + // This will block if the number of open files is already at the limit + pub fn before(limiter: *Limiter) void { + limiter.chan.put(false); } - pub fn readDir(context: *Context, path: string) []string { - return context.readdir(path); + pub fn after(limiter: *Limiter) void { + _ = await limiter.chan.get(); } }; - } -}; -pub const FileNotFound = struct {}; + fn readdir(fs: *RealFS, dir: string) !DirEntry { + fs.limiter.before(); + defer fs.limiter.after(); + + var handle = try std.fs.openDirAbsolute(dir, std.fs.Dir.OpenDirOptions{ .iterate = true, .access_sub_paths = true }); + defer handle.close(); + + var iter: std.fs.Dir.Iterator = handle.iterate(); + var dir = DirEntry{ .data = DirEntry.EntryMap.init(fs.allocator) }; + errdefer dir.deinit(); + while (try iter.next()) |_entry| { + const entry: std.fs.Dir.Entry = _entry; + var kind: Entry.Kind = undefined; + switch (entry.kind) { + Directory => { + kind = Entry.Kind.dir; + }, + SymLink => { + // This might be wrong! + kind = Entry.Kind.file; + }, + File => { + kind = Entry.Kind.file; + }, + else => { + continue; + }, + } + + // entry.name only lives for the duration of the iteration + var name = try fs.allocator.alloc(u8, entry.name.len); + for (entry.name) |c, i| { + name[i] = std.ascii.toLower(c); + } + try dir.data.put(name, Entry{ + .base = name, + .dir = dir, + .mutex = Mutex{}, + // Call "stat" lazily for performance. The "@material-ui/icons" package + // contains a directory with over 11,000 entries in it and running "stat" + // for each entry was a big performance issue for that package. + .need_stat = true, + .cache = Entry.Cache{ + .symlink = if (entry.kind == std.fs.Dir.Entry.Kind.SymLink) (try fs.allocator.dupe(u8, name)) else "", + .kind = kind, + }, + }); + } + // Copy at the bottom here so in the event of an error, we don't deinit the dir string. + dir.dir = dir; + return dir; + } + + fn readDirectoryError(fs: *RealFS, dir: string, err: anyerror) !void { + if (fs.watcher) |watcher| { + var hold = fs.watcher_mutex.acquire(); + defer hold.release(); + try watcher.put(dir, WatchData{ .state = .dir_missing }); + } + + if (!fs.do_not_cache_entries) { + var hold = fs.entries_mutex.acquire(); + defer hold.release(); + + try fs.entries.put(dir, EntriesOption{ + .err = DirEntry.Err{ .original_err = err, .canonical_err = err }, + }); + } + } + pub fn readDirectory(fs: *RealFS, dir: string) !EntriesOption { + if (!fs.do_not_cache_entries) { + var hold = fs.entries_mutex.acquire(); + defer hold.release(); + + // First, check the cache + if (fs.entries.get(dir)) |dir| { + return EntriesOption{ .entries = dir }; + } + } + + // Cache miss: read the directory entries + const entries = fs.readdir(dir) catch |err| return (try fs.readDirectoryError(dir, err)); + + if (fs.watcher) |watcher| { + var hold = fs.watcher_mutex.acquire(); + defer hold.release(); + var _entries = entries.data.items(); + const names = try fs.allocator.alloc([]const u8, _entries.len); + for (_entries) |entry, i| { + names[i] = try fs.allocator.dupe(u8, entry.key); + } + strings.sortAsc(names); + + try watcher.put( + try fs.allocator.dupe(u8, dir), + WatchData{ .dir_entries = names, .state = .dir_has_entries }, + ); + } + + if (!fs.do_not_cache_entries) { + var hold = fs.entries_mutex.acquire(); + defer hold.release(); + + try fs.entries.put(dir, EntriesOption{ + .err = DirEntry.Err{ .original_err = err, .canonical_err = err }, + }); + } + + return entries; + } + + fn readFileError(fs: *RealFS, path: string, err: anyerror) !void { + if (fs.watcher) |watcher| { + var hold = fs.watcher_mutex.acquire(); + defer hold.release(); + var res = try watcher.getOrPutValue(path, WatchData{ .state = .file_missing }); + res.value.state = .file_missing; + } + + return err; + } + + pub fn readFile(fs: *RealFS, path: string) !File { + fs.limiter.before(); + defer fs.limiter.after(); + + const file: std.fs.File = std.fs.openFileAbsolute(path, std.fs.File.OpenFlags{ .read = true, .write = false }) catch |err| return fs.readFileError(path, err); + defer file.close(); + + // return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); + // TODO: this causes an extra call to .stat, do it manually and cache the results ourself. + const size = try file.getEndPos() catch |err| return fs.readFileError(path, err); + const file_contents: []u8 = file.readToEndAllocOptions(fs.allocator, size, size, @alignOf(u8), null) catch |err| return fs.readFileError(path, err); + + if (fs.watcher) |watcher| { + var hold = fs.watcher_mutex.acquire(); + defer hold.release(); + var res = try watcher.getOrPutValue(path, WatchData{}); + res.value.state = .file_need_mod_key; + res.value.file_contents = file_contents; + } + + return File{ .path = Path.init(path), .contents = file_contents }; + } + + pub fn kind(fs: *RealFS, _dir: string, base: string) !Entry.Cache { + var dir = _dir; + var combo = [2]string{ dir, base }; + var entry_path = try std.fs.path.join(fs.allocator, &combo); + defer fs.allocator.free(entry_path); + + fs.limiter.before(); + defer fs.limiter.after(); + + const file = try std.fs.openFileAbsolute(entry_path, .{ .read = true, .write = false }); + defer file.close(); + const stat = try file.stat(); + var kind = stat.kind; + var cache = Entry.Cache{ .kind = Entry.Kind.file, .symlink = "" }; + + if (kind == .Symlink) { + // windows has a max filepath of 255 chars + // we give it a little longer for other platforms + var out_buffer = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + var out_slice = &out_buffer; + var symlink = entry_path; + var links_walked: u8 = 0; + + while (links_walked < 255) : (links_walked += 1) { + var link = try std.os.readLink(symlink, out_buffer); + + if (!std.fs.path.isAbsolute(link)) { + combo[0] = dir; + combo[1] = link; + if (link.ptr != out_slice.ptr) { + fs.allocator.free(link); + } + link = std.fs.path.join(fs.allocator, &combo) catch return cache; + } + // TODO: do we need to clean the path? + symlink = link; + + const file2 = std.fs.openFileAbsolute(symlink, File.OpenFlags{ .read = true, .write = false }) catch return cache; + defer file2.close(); + + const stat2 = file2.stat() catch return cache; + + // Re-run "lstat" on the symlink target + mode = stat2.mode; + if (mode == .Symlink) { + break; + } + dir = std.fs.path.dirname(link) orelse return cache; + } + + if (links_walked > 255) { + return cache; + } + } + + if (mode == .Directory) { + kind = Entry.Kind.dir; + } else { + kind = Entry.Kind.file; + } + cache.kind = kind; + cache.symlink = symlink; + + return cache; + } + + // // Stores the file entries for directories we've listed before + // entries_mutex: std.Mutex + // entries map[string]entriesOrErr + + // // If true, do not use the "entries" cache + // doNotCacheEntries bool + }; + + pub const Implementation = comptime { + switch (build_target) { + .wasi, .native => RealFS, + .wasm => WasmFS, + } + }; +}; pub const FileSystemEntry = union(FileSystemEntry.Kind) { file: File, @@ -145,6 +602,11 @@ pub const Path = struct { namespace: string, name: PathName, + // TODO: + pub fn normalize(str: string) string { + return str; + } + pub fn init(text: string) Path { return Path{ .pretty = text, .text = text, .namespace = "file", .name = PathName.init(text) }; } diff --git a/src/global.zig b/src/global.zig index 9ac540311..478035519 100644 --- a/src/global.zig +++ b/src/global.zig @@ -1,7 +1,20 @@ const std = @import("std"); pub usingnamespace @import("strings.zig"); -pub const isWasm = comptime std.Target.current.isWasm(); +pub const BuildTarget = enum { native, wasm, wasi }; +pub const build_target: BuildTarget = comptime { + if (std.Target.current.isWasm() and std.Target.current.getOsTag() == .wasi) { + return BuildTarget.wasi; + } else if (std.Target.current.isWasm()) { + return BuildTarget.wasm; + } else { + return BuildTarget.native; + } +}; + +pub const isWasm = build_target == .wasm; +pub const isNative = build_target == .native; +pub const isWasi = build_target == .wasi; pub const Output = struct { var source: *Source = undefined; diff --git a/src/js_ast.zig b/src/js_ast.zig index 4162fab28..95dd3b96f 100644 --- a/src/js_ast.zig +++ b/src/js_ast.zig @@ -1380,6 +1380,35 @@ pub const Expr = struct { loc: logger.Loc, data: Data, + pub const Query = struct { expr: Expr, loc: logger.Loc }; + + pub fn getProperty(expr: *Expr, name: string) ?Query { + const obj: *E.Object = expr.data.e_object orelse return null; + + for (obj.properties) |prop| { + const value = prop.value orelse continue; + const key = prop.key orelse continue; + const key_str: *E.String = key.data.e_string orelse continue; + if (key_str.eql(string, name)) { + return Query{ .expr = value, .loc = key.loc }; + } + } + + return null; + } + + pub fn getString(expr: *Expr, allocator: *std.mem.Allocator) !?string { + const key_str: *E.String = expr.data.e_string orelse return null; + + return if (key_str.isUTF8()) key_str.value else key_str.string(allocator); + } + + pub fn getBool(expr: *Expr, allocator: *std.mem.Allocator) ?bool { + const obj: *E.Boolean = expr.data.e_boolean orelse return null; + + return obj.value; + } + pub const EFlags = enum { none, ts_decorator }; const Serializable = struct { diff --git a/src/options.zig b/src/options.zig index eaf57e6f8..f4c2a85c1 100644 --- a/src/options.zig +++ b/src/options.zig @@ -7,6 +7,51 @@ usingnamespace @import("global.zig"); const assert = std.debug.assert; +pub const Platform = enum { + node, + browser, + neutral, + + const MAIN_FIELD_NAMES = [_]string{ "browser", "module", "main" }; + pub const DefaultMainFields: std.EnumArray(Platform, []string) = comptime { + var array = std.EnumArray(Platform, []string); + + // 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{ MAIN_FIELD_NAMES[1], MAIN_FIELD_NAMES[2] })); + + // 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. + array.set(Platform.browser, &([_]string{ MAIN_FIELD_NAMES[0], MAIN_FIELD_NAMES[1], MAIN_FIELD_NAMES[2] })); + + // 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, &([_]string{})); + + return array; + }; +}; + pub const Loader = enum { jsx, js, @@ -28,6 +73,17 @@ pub const defaultLoaders = std.ComptimeStringMap(Loader, .{ }); pub const JSX = struct { + pub const Pragma = struct { + factory: string = "React.createElement", + fragment: string = "React.Fragment", + runtime: JSX.Runtime = JSX.Runtime.automatic, + + /// Facilitates automatic JSX importing + /// Set on a per file basis like this: + /// /** @jsxImportSource @emotion/core */ + import_source: string = "react", + }; + parse: bool = true, factory: string = "createElement", fragment: string = "Fragment", diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig new file mode 100644 index 000000000..81921e510 --- /dev/null +++ b/src/resolver/resolve_path.zig @@ -0,0 +1,82 @@ +// https://github.com/MasterQ32/ftz/blob/3183b582211f8e38c1c3363c56753026ca45c11f/src/main.zig#L431-L509 +// Thanks, Felix! We should get this into std perhaps. + +const std = @import("std"); + +/// Resolves a unix-like path and removes all "." and ".." from it. Will not escape the root and can be used to sanitize inputs. +pub fn resolvePath(buffer: []u8, src_path: []const u8) error{BufferTooSmall}![]u8 { + if (buffer.len == 0) + return error.BufferTooSmall; + if (src_path.len == 0) { + buffer[0] = '/'; + return buffer[0..1]; + } + + var end: usize = 0; + buffer[0] = '/'; + + var iter = std.mem.tokenize(src_path, "/"); + while (iter.next()) |segment| { + if (std.mem.eql(u8, segment, ".")) { + continue; + } else if (std.mem.eql(u8, segment, "..")) { + while (true) { + if (end == 0) + break; + if (buffer[end] == '/') { + break; + } + end -= 1; + } + } else { + if (end + segment.len + 1 > buffer.len) + return error.BufferTooSmall; + + const start = end; + buffer[end] = '/'; + end += segment.len + 1; + std.mem.copy(u8, buffer[start + 1 .. end], segment); + } + } + + return if (end == 0) + buffer[0 .. end + 1] + else + buffer[0..end]; +} + +fn testResolve(expected: []const u8, input: []const u8) !void { + var buffer: [1024]u8 = undefined; + + const actual = try resolvePath(&buffer, input); + std.testing.expectEqualStrings(expected, actual); +} + +test "resolvePath" { + try testResolve("/", ""); + try testResolve("/", "/"); + try testResolve("/", "////////////"); + + try testResolve("/a", "a"); + try testResolve("/a", "/a"); + try testResolve("/a", "////////////a"); + try testResolve("/a", "////////////a///"); + + try testResolve("/a/b/c/d", "/a/b/c/d"); + + try testResolve("/a/b/d", "/a/b/c/../d"); + + try testResolve("/", ".."); + try testResolve("/", "/.."); + try testResolve("/", "/../../../.."); + try testResolve("/a/b/c", "a/b/c/"); + + try testResolve("/new/date.txt", "/new/../../new/date.txt"); +} + +test "resolvePath overflow" { + var buf: [1]u8 = undefined; + + std.testing.expectEqualStrings("/", try resolvePath(&buf, "/")); + std.testing.expectError(error.BufferTooSmall, resolvePath(&buf, "a")); // will resolve to "/a" +} diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig new file mode 100644 index 000000000..138b1b72f --- /dev/null +++ b/src/resolver/resolver.zig @@ -0,0 +1,219 @@ +usingnamespace @import("../global.zig"); +const ast = @import("../ast.zig"); +const logger = @import("../logger.zig"); +const options = @import("../options.zig"); +const fs = @import("../fs.zig"); +const std = @import("std"); + +pub const SideEffectsData = struct { + source: *logger.Source, + range: logger.Range, + + // If true, "sideEffects" was an array. If false, "sideEffects" was false. + is_side_effects_array_in_json: bool = false, +}; + +pub const DirInfo = struct { + // These objects are immutable, so we can just point to the parent directory + // and avoid having to lock the cache again + parent: ?*DirInfo = null, + + // A pointer to the enclosing dirInfo with a valid "browser" field in + // package.json. We need this to remap paths after they have been resolved. + enclosing_browser_scope: *?DirInfo = null, + + abs_path: string, + entries: fs.FileSystem.DirEntry, + has_node_modules: bool = false, // Is there a "node_modules" subdirectory? + package_json: ?*PackageJSON, // Is there a "package.json" file? + ts_config_json: ?*TSConfigJSON, // Is there a "tsconfig.json" file in this directory or a parent directory? + abs_real_path: string = "", // If non-empty, this is the real absolute path resolving any symlinks + +}; + +pub const Resolver = struct { + opts: options.TransformOptions, + fs: *fs.FileSystem, + log: *logger.Log, + allocator: *std.mem.Allocator, + + debug_logs: ?DebugLogs = null, + + // 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), + + // A special filtered import order for CSS "@import" imports. + // + // The "resolve extensions" setting determines the order of implicit + // extensions to try when resolving imports with the extension omitted. + // Sometimes people create a JavaScript/TypeScript file and a CSS file with + // the same name when they create a component. At a high level, users expect + // implicit extensions to resolve to the JS file when being imported from JS + // and to resolve to the CSS file when being imported from CSS. + // + // Different bundlers handle this in different ways. Parcel handles this by + // having the resolver prefer the same extension as the importing file in + // front of the configured "resolve extensions" order. Webpack's "css-loader" + // plugin just explicitly configures a special "resolve extensions" order + // consisting of only ".css" for CSS files. + // + // It's unclear what behavior is best here. What we currently do is to create + // a special filtered version of the configured "resolve extensions" order + // for CSS files that filters out any extension that has been explicitly + // configured with a non-CSS loader. This still gives users control over the + // order but avoids the scenario where we match an import in a CSS file to a + // JavaScript-related file. It's probably not perfect with plugins in the + // picture but it's better than some alternatives and probably pretty good. + // atImportExtensionOrder []string + + // This mutex serves two purposes. First of all, it guards access to "dirCache" + // which is potentially mutated during path resolution. But this mutex is also + // necessary for performance. The "React admin" benchmark mysteriously runs + // twice as fast when this mutex is locked around the whole resolve operation + // instead of around individual accesses to "dirCache". For some reason, + // reducing parallelism in the resolver helps the rest of the bundler go + // faster. I'm not sure why this is but please don't change this unless you + // do a lot of testing with various benchmarks and there aren't any regressions. + mutex: std.Thread.Mutex, + + // This cache maps a directory path to information about that directory and + // all parent directories + dir_cache: std.StringHashMap(?*DirInfo), + + pub const DebugLogs = struct { + what: string = "", + indent: MutableString, + notes: std.ArrayList(logger.Data), + + pub fn init(allocator: *std.mem.Allocator) DebugLogs { + return .{ + .indent = MutableString.init(allocator, 0), + .notes = std.ArrayList(logger.Data).init(allocator), + }; + } + + pub fn deinit(d: DebugLogs) void { + var allocator = d.notes.allocator; + d.notes.deinit(); + d.indent.deinit(); + } + + pub fn increaseIndent(d: *DebugLogs) !void { + try d.indent.append(" "); + } + + pub fn decreaseIndent(d: *DebugLogs) !void { + d.indent.list.shrinkRetainingCapacity(d.indent.list.items.len - 1); + } + + pub fn addNote(d: *DebugLogs, _text: string) !void { + var text = _text; + const len = d.indent.len(); + if (len > 0) { + text = try d.notes.allocator.alloc(u8, text.len + d.indent.len); + std.mem.copy(u8, text, d.indent); + std.mem.copy(u8, text[d.indent.len..text.len], _text); + d.notes.allocator.free(_text); + } + + try d.notes.append(logger.rangeData(null, logger.Range.None, text)); + } + }; + + pub const PathPair = struct { + primary: logger.Path, + secondary: ?logger.Path = null, + }; + + pub const Result = struct { + path_pair: PathPair, + + jsx: options.JSX.Pragma = options.JSX.Pragma{}, + + // plugin_data: void + }; + + pub fn resolve(r: *Resolver, source_dir: string, import_path: string, kind: ast.ImportKind) Result {} + + fn dirInfoCached(r: *Resolver, path: string) !*DirInfo { + // First, check the cache + if (r.dir_cache.get(path)) |dir| { + return dir; + } + + const info = try r.dirInfoUncached(path); + + try r.dir_cache.put(path, info); + } + + fn dirInfoUncached(r: *Resolver, path: string) !?*DirInfo { + const rfs: r.fs.RealFS = r.fs.fs; + var parent: ?*DirInfo = null; + const parent_dir = std.fs.path.dirname(path) orelse return null; + if (!strings.eql(parent_dir, path)) { + parent = r.dirInfoCached(parent_dir); + } + + // List the directories + var _entries = try rfs.readDirectory(path); + var entries: @TypeOf(_entries.entries) = undefined; + if (std.meta.activeTag(_entries) == .err) { + // Just pretend this directory is empty if we can't access it. This is the + // case on Unix for directories that only have the execute permission bit + // set. It means we will just pass through the empty directory and + // continue to check the directories above it, which is now node behaves. + switch (_entries.err) { + fs.FileSystem.Error.EACCESS => { + entries = fs.FileSystem.DirEntry.empty(path, r.allocator); + }, + + // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves + // as if there is nothing there at all instead of causing an error due to + // the directory actually being a file. This is a workaround for situations + // where people try to import from a path containing a file as a parent + // directory. The "pnpm" package manager generates a faulty "NODE_PATH" + // list which contains such paths and treating them as missing means we just + // ignore them during path resolution. + fs.FileSystem.Error.ENOENT, + fs.FileSystem.Error.ENOTDIR, + => {}, + else => { + const pretty = r.prettyPath(fs.Path{ .text = path, .namespace = "file" }); + r.log.addErrorFmt( + null, + logger.Loc{}, + r.allocator, + "Cannot read directory \"{s}\": {s}", + .{ + pretty, + @errorName(err), + }, + ); + return null; + }, + } + } else { + entries = _entries.entries; + } + + var info = try r.allocator.create(DirInfo); + info.* = DirInfo{ + .abs_path = path, + .parent = parent_dir, + .entries = entries, + }; + + // A "node_modules" directory isn't allowed to directly contain another "node_modules" directory + var base = std.fs.path.basename(path); + if (!strings.eqlComptime(base, "node_modules")) { + if (entries.get("node_modules")) |entry| { + info.has_node_modules = entry.entry.kind(rfs) == .dir; + } + } + + // Propagate the browser scope into child directories + } +}; diff --git a/src/string_mutable.zig b/src/string_mutable.zig index a5b13ca2e..610f35a0a 100644 --- a/src/string_mutable.zig +++ b/src/string_mutable.zig @@ -14,6 +14,10 @@ pub const MutableString = struct { }; } + pub fn deinit(str: *MutableString) void { + str.list.deinit(str.allocator); + } + pub fn growIfNeeded(self: *MutableString, amount: usize) !void { try self.list.ensureUnusedCapacity(self.allocator, amount); } diff --git a/test.js b/test.js deleted file mode 100644 index 25f1a1736..000000000 --- a/test.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; - -const foo = { - object: { - nested: `foo1`, - }, - bar: 1, - // React: React, -}; - -const arrays = [1, 2, 3, "10", 200n, React.createElement("foo")]; - -function hi() { - console.log("We need to go deeper."); - function hey() { - hi(); - } -} - -class Foo { - get prop() { - return 1; - } - - set prop(v) { - this._v = v; - } - - static staticInstance() { - return "hi"; - } - - static get prop() { - return "yo"; - } - - static set prop(v) { - Foo.v = v; - } - - insance() {} - insanceWithArgs(arg, arg2) {} - insanceWithRestArgs(arg, arg2, ...arg3) {} -} - -try { - console.log("HI"); -} catch (e) { - console.log("HEY", e); -} - -if (true) { - for (let i = 0; i < 100; i++) { - console.log(); - } - console.log("development!"); -} diff --git a/test.jsx b/test.jsx deleted file mode 100644 index b0e737df3..000000000 --- a/test.jsx +++ /dev/null @@ -1,27 +0,0 @@ -var Button = () => { - return <div className="button">Button!</div>; -}; - -var Bar = () => { - return ( - <div prop={1}> - Plain text - <div> - ← A child div - <Button>Red</Button> - </div> - </div> - ); -}; - -var Triple = () => { - return ( - <div prop={1}> - Plain text - <div> - ← A child div - <Button>Red</Button> - </div> - </div> - ); -}; |