diff options
Diffstat (limited to 'src/bundler.zig')
-rw-r--r-- | src/bundler.zig | 437 |
1 files changed, 265 insertions, 172 deletions
diff --git a/src/bundler.zig b/src/bundler.zig index d7abbd84c..f4777c345 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -28,34 +28,72 @@ const Linker = linker.Linker; const Timer = @import("./timer.zig"); pub const ServeResult = struct { - value: Value, - free: bool = true, + file: options.OutputFile, mime_type: MimeType, - - // Either we: - // - send pre-buffered asset body - // - stream a file from the file system - pub const Value = union(Tag) { - file: File, - build: options.OutputFile, - none: u0, - - pub const Tag = enum { - file, - build, - none, - }; - - pub const File = struct { - absolute_path: string, - handle: std.fs.File, - }; - }; }; // const BundleMap = pub const ResolveResults = ThreadSafeHashMap.ThreadSafeStringHashMap(Resolver.Resolver.Result); pub const ResolveQueue = std.fifo.LinearFifo(Resolver.Resolver.Result, std.fifo.LinearFifoBufferType.Dynamic); + +// How it works end-to-end +// 1. Resolve a file path from input using the resolver +// 2. Look at the extension of that file path, and determine a loader +// 3. If the loader is .js, .jsx, .ts, .tsx, or .json, run it through our JavaScript Parser +// IF serving via HTTP and it's parsed without errors: +// 4. If parsed without errors, generate a strong ETag & write the output directly to the network socket in the Printer. +// 7. Else, write any errors to error page +// IF writing to disk AND it's parsed without errors: +// 4. Write the output to a temporary file. +// Why? Two reasons. +// 1. At this point, we don't know what the best output path is. +// Most of the time, you want the shortest common path, which you can't know until you've +// built & resolved all paths. +// Consider this directory tree: +// - /Users/jarred/Code/app/src/index.tsx +// - /Users/jarred/Code/app/src/Button.tsx +// - /Users/jarred/Code/app/assets/logo.png +// - /Users/jarred/Code/app/src/Button.css +// - /Users/jarred/Code/app/node_modules/react/index.js +// - /Users/jarred/Code/app/node_modules/react/cjs/react.development.js +// Remember that we cannot know which paths need to be resolved without parsing the JavaScript. +// If we stopped here: /Users/jarred/Code/app/src/Button.tsx +// We would choose /Users/jarred/Code/app/src/ as the directory +// Then, that would result in a directory structure like this: +// - /Users/jarred/Code/app/src/Users/jarred/Code/app/node_modules/react/cjs/react.development.js +// Which is absolutely insane +// +// 2. We will need to write to disk at some point! +// - If we delay writing to disk, we need to print & allocate a potentially quite large +// buffer (react-dom.development.js is 550 KB) +// ^ This is how it used to work! +// - If we delay printing, we need to keep the AST around. Which breaks all our +// recycling logic since that could be many many ASTs. +// 5. Once all files are written, determine the shortest common path +// 6. Move all the temporary files to their intended destinations +// IF writing to disk AND it's a file-like loader +// 4. Hash the contents +// - rewrite_paths.put(absolute_path, hash(file(absolute_path))) +// 5. Resolve any imports of this file to that hash(file(absolute_path)) +// 6. Append to the files array with the new filename +// 7. When parsing & resolving is over, just copy the file. +// - on macOS, ensure it does an APFS shallow clone so that doesn't use disk space +// IF serving via HTTP AND it's a file-like loader: +// 4. Hash the metadata ${absolute_path}-${fstat.mtime}-${fstat.size} +// 5. Use a deterministic prefix so we know what file to look for without copying it +// Example scenario: +// GET /logo-SIU3242.png +// 404 Not Found because there is no file named "logo-SIu3242.png" +// Instead, we can do this: +// GET /public/SIU3242/logo.png +// Our server sees "/public/" and knows the next segment will be a token +// which lets it ignore that when resolving the absolute path on disk +// 6. Compare the current hash with the expected hash +// 7. IF does not match, do a 301 Temporary Redirect to the new file path +// This adds an extra network request for outdated files, but that should be uncommon. +// 7. IF does match, serve it with that hash as a weak ETag +// 8. This should also just work unprefixed, but that will be served Cache-Control: private, no-store + pub const Bundler = struct { options: options.BundleOptions, log: *logger.Log, @@ -119,61 +157,120 @@ pub const Bundler = struct { ); } - pub fn buildWithResolveResult(bundler: *Bundler, resolve_result: Resolver.Resolver.Result) !?options.OutputFile { + pub fn resetStore(bundler: *Bundler) void { + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + } + + pub fn buildWithResolveResult( + bundler: *Bundler, + resolve_result: Resolver.Resolver.Result, + allocator: *std.mem.Allocator, + loader: options.Loader, + comptime Writer: type, + writer: Writer, + ) !usize { + if (resolve_result.is_external) { + return 0; + } + + errdefer bundler.resetStore(); + + var file_path = resolve_result.path_pair.primary; + file_path.pretty = allocator.dupe(u8, bundler.fs.relativeTo(file_path.text)) catch unreachable; + + var old_bundler_allocator = bundler.allocator; + bundler.allocator = allocator; + defer bundler.allocator = old_bundler_allocator; + var result = bundler.parse(allocator, file_path, loader, resolve_result.dirname_fd) orelse { + bundler.resetStore(); + return 0; + }; + var old_linker_allocator = bundler.linker.allocator; + defer bundler.linker.allocator = old_linker_allocator; + bundler.linker.allocator = allocator; + try bundler.linker.link(file_path, &result); + + return try bundler.print( + result, + Writer, + writer, + ); + // output_file.version = if (resolve_result.is_from_node_modules) resolve_result.package_json_version else null; + + } + + pub fn buildWithResolveResultEager(bundler: *Bundler, resolve_result: Resolver.Resolver.Result) !?options.OutputFile { if (resolve_result.is_external) { return null; } + errdefer js_ast.Expr.Data.Store.reset(); errdefer js_ast.Stmt.Data.Store.reset(); // Step 1. Parse & scan const loader = bundler.options.loaders.get(resolve_result.path_pair.primary.name.ext) orelse .file; var file_path = resolve_result.path_pair.primary; - file_path.pretty = Linker.relative_paths_list.append(bundler.fs.relativeTo(file_path.text)) catch unreachable; - var result = bundler.parse(file_path, loader, resolve_result.dirname_fd) orelse { - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - return null; - }; - try bundler.linker.link(file_path, &result); + switch (loader) { + .jsx, .tsx, .js, .json => { + var result = bundler.parse(bundler.allocator, file_path, loader, resolve_result.dirname_fd) orelse { + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); + return null; + }; - var output_file = try bundler.print( - result, - ); - // output_file.version = if (resolve_result.is_from_node_modules) resolve_result.package_json_version else null; + try bundler.linker.link(file_path, &result); + var output_file = options.OutputFile{ + .input = file_path, + .loader = loader, + .value = undefined, + }; + + const output_dir = bundler.options.output_dir_handle.?; + if (std.fs.path.dirname(file_path.pretty)) |dirname| { + try output_dir.makePath(dirname); + } + + var file = try output_dir.createFile(file_path.pretty, .{}); + output_file.size = try bundler.print( + result, + js_printer.FileWriter, + js_printer.NewFileWriter(file), + ); - return output_file; + var file_op = options.OutputFile.FileOperation.fromFile(file.handle, file_path.pretty); + file_op.dir = output_dir.fd; + file_op.fd = file.handle; + + if (bundler.fs.fs.needToCloseFiles()) { + file.close(); + file_op.fd = 0; + } + file_op.is_tmpdir = false; + output_file.value = .{ .move = file_op }; + return output_file; + }, + // TODO: + else => { + return null; + }, + } } pub fn print( bundler: *Bundler, result: ParseResult, - ) !options.OutputFile { - var allocator = bundler.allocator; - var parts = &([_]string{result.source.path.text}); - var abs_path = bundler.fs.abs(parts); - var rel_path = bundler.fs.relativeTo(abs_path); - var pathname = Fs.PathName.init(rel_path); - - if (bundler.options.out_extensions.get(pathname.ext)) |ext| { - pathname.ext = ext; - } - - var stack_fallback = std.heap.stackFallback(1024, bundler.allocator); - - var stack = stack_fallback.get(); - var _out_path = std.fmt.allocPrint(stack, "{s}{s}{s}{s}", .{ pathname.dir, std.fs.path.sep_str, pathname.base, pathname.ext }) catch unreachable; - defer stack.free(_out_path); - var out_path = bundler.fs.filename_store.append(_out_path) catch unreachable; - + comptime Writer: type, + writer: Writer, + ) !usize { const ast = result.ast; - var symbols: [][]js_ast.Symbol = &([_][]js_ast.Symbol{ast.symbols}); - const print_result = try js_printer.printAst( - allocator, + return try js_printer.printAst( + Writer, + writer, ast, js_ast.Symbol.Map.initList(symbols), &result.source, @@ -185,22 +282,15 @@ pub const Bundler = struct { }, &bundler.linker, ); - // allocator.free(result.source.contents); - - return options.OutputFile{ - .path = out_path, - .contents = print_result.js, - }; } pub const ParseResult = struct { source: logger.Source, loader: options.Loader, - ast: js_ast.Ast, }; - pub fn parse(bundler: *Bundler, path: Fs.Path, loader: options.Loader, dirname_fd: StoredFileDescriptorType) ?ParseResult { + pub fn parse(bundler: *Bundler, allocator: *std.mem.Allocator, path: Fs.Path, loader: options.Loader, dirname_fd: StoredFileDescriptorType) ?ParseResult { if (enableTracing) { bundler.timer.start(); } @@ -212,6 +302,7 @@ pub const Bundler = struct { } var result: ParseResult = undefined; const entry = bundler.resolver.caches.fs.readFile(bundler.fs, path.text, dirname_fd) catch return null; + const source = logger.Source.initFile(Fs.File{ .path = path, .contents = entry.contents }, bundler.allocator) catch return null; switch (loader) { @@ -219,7 +310,7 @@ pub const Bundler = struct { var jsx = bundler.options.jsx; jsx.parse = loader.isJSX(); var opts = js_parser.Parser.Options.init(jsx, loader); - const value = (bundler.resolver.caches.js.parse(bundler.allocator, opts, bundler.options.define, bundler.log, &source) catch null) orelse return null; + const value = (bundler.resolver.caches.js.parse(allocator, opts, bundler.options.define, bundler.log, &source) catch null) orelse return null; return ParseResult{ .ast = value, .source = source, @@ -227,14 +318,14 @@ pub const Bundler = struct { }; }, .json => { - var expr = json_parser.ParseJSON(&source, bundler.log, bundler.allocator) catch return null; - var stmt = js_ast.Stmt.alloc(bundler.allocator, js_ast.S.ExportDefault{ + var expr = json_parser.ParseJSON(&source, bundler.log, allocator) catch return null; + var stmt = js_ast.Stmt.alloc(allocator, js_ast.S.ExportDefault{ .value = js_ast.StmtOrExpr{ .expr = expr }, .default_name = js_ast.LocRef{ .loc = logger.Loc{}, .ref = Ref{} }, }, logger.Loc{ .start = 0 }); - var stmts = bundler.allocator.alloc(js_ast.Stmt, 1) catch unreachable; + var stmts = allocator.alloc(js_ast.Stmt, 1) catch unreachable; stmts[0] = stmt; - var parts = bundler.allocator.alloc(js_ast.Part, 1) catch unreachable; + var parts = allocator.alloc(js_ast.Part, 1) catch unreachable; parts[0] = js_ast.Part{ .stmts = stmts }; return ParseResult{ @@ -282,6 +373,7 @@ pub const Bundler = struct { defer bundler.log = original_bundler_logger; defer bundler.resolver.log = original_resolver_logger; bundler.log = log; + bundler.linker.allocator = allocator; bundler.resolver.log = log; // Resolving a public file has special behavior @@ -353,22 +445,32 @@ pub const Bundler = struct { break; } - if (_file) |file| { - const _parts = [_]string{ bundler.options.public_dir, relative_unrooted_path }; + if (_file) |*file| { + var stat = try file.stat(); + var absolute_path = resolve_path.joinAbs(bundler.options.public_dir, .auto, relative_unrooted_path); + + if (stat.kind == .SymLink) { + absolute_path = try std.fs.realpath(absolute_path, &tmp_buildfile_buf); + file.close(); + file.* = try std.fs.openFileAbsolute(absolute_path, .{ .read = true }); + stat = try file.stat(); + } + + if (stat.kind != .File) { + file.close(); + return error.NotFile; + } + return ServeResult{ - .value = ServeResult.Value{ .file = .{ - .absolute_path = try bundler.fs.joinAlloc(allocator, &_parts), - .handle = file, - } }, - .mime_type = MimeType.byExtension(extension), + .file = options.OutputFile.initFile(file.*, absolute_path, stat.size), + .mime_type = MimeType.byExtension(std.fs.path.extension(absolute_path)[1..]), }; } } if (strings.eqlComptime(relative_path, "__runtime.js")) { return ServeResult{ - .free = false, - .value = .{ .build = .{ .path = "__runtime.js", .contents = runtime.SourceContent } }, + .file = options.OutputFile.initBuf(runtime.SourceContent, "__runtime.js", .js), .mime_type = MimeType.javascript, }; } @@ -394,20 +496,27 @@ pub const Bundler = struct { const resolved = (try bundler.resolver.resolve(bundler.fs.top_level_dir, absolute_path, .entry_point)); const loader = bundler.options.loaders.get(resolved.path_pair.primary.name.ext) orelse .file; - const output = switch (loader) { - .js, .jsx, .ts, .tsx, .json => ServeResult.Value{ - .build = (try bundler.buildWithResolveResult(resolved)) orelse return error.BuildFailed, - }, - else => ServeResult.Value{ .file = ServeResult.Value.File{ - .absolute_path = resolved.path_pair.primary.text, - .handle = try std.fs.openFileAbsolute(resolved.path_pair.primary.text, .{ .read = true, .write = false }), - } }, - }; - return ServeResult{ - .value = output, - .mime_type = MimeType.byLoader(loader, resolved.path_pair.primary.name.ext), - }; + switch (loader) { + .js, .jsx, .ts, .tsx, .json => { + return ServeResult{ + .file = options.OutputFile.initPending(loader, resolved), + .mime_type = MimeType.byLoader( + loader, + bundler.options.out_extensions.get(resolved.path_pair.primary.name.ext) orelse resolved.path_pair.primary.name.ext, + ), + }; + }, + else => { + var abs_path = resolved.path_pair.primary.text; + const file = try std.fs.openFileAbsolute(abs_path, .{ .read = true }); + var stat = try file.stat(); + return ServeResult{ + .file = options.OutputFile.initFile(file, abs_path, stat.size), + .mime_type = MimeType.byLoader(loader, abs_path), + }; + }, + } } pub fn bundle( @@ -418,6 +527,8 @@ pub const Bundler = struct { var bundler = try Bundler.init(allocator, log, opts); bundler.configureLinker(); + if (bundler.options.write and bundler.options.output_dir.len > 0) {} + // 100.00 µs std.fifo.LinearFifo(resolver.resolver.Result,std.fifo.LinearFifoBufferType { .Dynamic = {}}).writeItemAssumeCapacity if (bundler.options.resolve_mode != .lazy) { try bundler.resolve_queue.ensureUnusedCapacity(24); @@ -435,30 +546,8 @@ pub const Bundler = struct { var entry_point_i: usize = 0; for (bundler.options.entry_points) |_entry| { var entry: string = _entry; - // if (!std.fs.path.isAbsolute(_entry)) { - // const _paths = [_]string{ bundler.fs.top_level_dir, _entry }; - // entry = std.fs.path.join(allocator, &_paths) catch unreachable; - // } else { - // entry = allocator.dupe(u8, _entry) catch unreachable; - // } - - // const dir = std.fs.path.dirname(entry) orelse continue; - // const base = std.fs.path.basename(entry); - - // var dir_entry = try rfs.readDirectory(dir); - // if (std.meta.activeTag(dir_entry) == .err) { - // log.addErrorFmt(null, logger.Loc.Empty, allocator, "Failed to read directory: {s} - {s}", .{ dir, @errorName(dir_entry.err.original_err) }) catch unreachable; - // continue; - // } - - // const file_entry = dir_entry.entries.get(base) orelse continue; - // if (file_entry.entry.kind(rfs) != .file) { - // continue; - // } if (!strings.startsWith(entry, "./")) { - // allocator.free(entry); - // Entry point paths without a leading "./" are interpreted as package // paths. This happens because they go through general path resolution // like all other import paths so that plugins can run on them. Requiring @@ -508,7 +597,7 @@ pub const Bundler = struct { while (bundler.resolve_queue.readItem()) |item| { js_ast.Expr.Data.Store.reset(); js_ast.Stmt.Data.Store.reset(); - const output_file = bundler.buildWithResolveResult(item) catch continue orelse continue; + const output_file = bundler.buildWithResolveResultEager(item) catch continue orelse continue; bundler.output_files.append(output_file) catch unreachable; } }, @@ -522,10 +611,9 @@ pub const Bundler = struct { // } if (bundler.linker.any_needs_runtime) { - try bundler.output_files.append(options.OutputFile{ - .path = bundler.linker.runtime_source_path, - .contents = runtime.SourceContent, - }); + try bundler.output_files.append( + options.OutputFile.initBuf(runtime.SourceContent, bundler.linker.runtime_source_path, .js), + ); } if (enableTracing) { @@ -538,7 +626,9 @@ pub const Bundler = struct { ); } - return try options.TransformResult.init(try allocator.dupe(u8, bundler.result.outbase), bundler.output_files.toOwnedSlice(), log, allocator); + var final_result = try options.TransformResult.init(try allocator.dupe(u8, bundler.result.outbase), bundler.output_files.toOwnedSlice(), log, allocator); + final_result.root_dir = bundler.options.output_dir_handle; + return final_result; } }; @@ -601,76 +691,78 @@ pub const Transformer = struct { var ulimit: usize = Fs.FileSystem.RealFS.adjustUlimit(); var care_about_closing_files = !(FeatureFlags.store_file_descriptors and opts.entry_points.len * 2 < ulimit); - for (opts.entry_points) |entry_point, i| { - if (use_arenas) { - arena = std.heap.ArenaAllocator.init(allocator); - chosen_alloc = &arena.allocator; - } - defer { - if (use_arenas) { - arena.deinit(); - } - } + for (opts.entry_points) |entry_point, i| {} - var _log = logger.Log.init(allocator); - var __log = &_log; - const absolutePath = resolve_path.joinAbs(cwd, .auto, entry_point); + return try options.TransformResult.init(output_dir, output_files.toOwnedSlice(), log, allocator); + } - const file = try std.fs.openFileAbsolute(absolutePath, std.fs.File.OpenFlags{ .read = true }); - defer { - if (care_about_closing_files) { - file.close(); - } - } + pub fn processEntryPoint( + transformer: *Transformer, + entry_point: string, + i: usize, + comptime write_destination_type: options.WriteDestination, + ) !void { + var allocator = transformer.allocator; + var log = transformer.log; - const stat = try file.stat(); + var _log = logger.Log.init(allocator); + var __log = &_log; + const absolutePath = resolve_path.joinAbs(cwd, .auto, entry_point); - // 1 byte sentinel - const code = try file.readToEndAlloc(allocator, stat.size); - defer { - if (_log.msgs.items.len == 0) { - allocator.free(code); - } - _log.appendTo(log) catch {}; + const file = try std.fs.openFileAbsolute(absolutePath, std.fs.File.OpenFlags{ .read = true }); + defer { + if (care_about_closing_files) { + file.close(); } - const _file = Fs.File{ .path = Fs.Path.init(entry_point), .contents = code }; - var source = try logger.Source.initFile(_file, chosen_alloc); - var loader: options.Loader = undefined; - if (use_default_loaders) { - loader = options.defaultLoaders.get(std.fs.path.extension(absolutePath)) orelse continue; - } else { - loader = options.Loader.forFileName( - entry_point, - loader_map, - ) orelse continue; + } + + const stat = try file.stat(); + + const code = try file.readToEndAlloc(allocator, stat.size); + defer { + if (_log.msgs.items.len == 0) { + allocator.free(code); } + _log.appendTo(log) catch {}; + } + const _file = Fs.File{ .path = Fs.Path.init(entry_point), .contents = code }; + var source = try logger.Source.initFile(_file, allocator); + var loader: options.Loader = undefined; + if (use_default_loaders) { + loader = options.defaultLoaders.get(std.fs.path.extension(absolutePath)) orelse return; + } else { + loader = options.Loader.forFileName( + entry_point, + loader_map, + ) orelse return; + } - jsx.parse = loader.isJSX(); + jsx.parse = loader.isJSX(); - const parser_opts = js_parser.Parser.Options.init(jsx, loader); - var _source = &source; - const res = _transform(chosen_alloc, allocator, __log, parser_opts, loader, define, _source) catch continue; + const parser_opts = js_parser.Parser.Options.init(jsx, loader); + var _source = &source; - const relative_path = resolve_path.relative(cwd, absolutePath); - const out_path = resolve_path.joinAbs2(cwd, .auto, absolutePath, relative_path); - try output_files.append(options.OutputFile{ .path = allocator.dupe(u8, out_path) catch continue, .contents = res.js }); - js_ast.Expr.Data.Store.reset(); - js_ast.Stmt.Data.Store.reset(); - } + const relative_path = resolve_path.relative(cwd, absolutePath); + const out_path = resolve_path.joinAbs(cwd, .auto, absolutePath, relative_path); - return try options.TransformResult.init(output_dir, output_files.toOwnedSlice(), log, allocator); + switch (write_destination_type) {} + + try output_files.append(); + js_ast.Expr.Data.Store.reset(); + js_ast.Stmt.Data.Store.reset(); } pub fn _transform( allocator: *std.mem.Allocator, - result_allocator: *std.mem.Allocator, log: *logger.Log, opts: js_parser.Parser.Options, loader: options.Loader, - define: *Define, - source: *logger.Source, - ) !js_printer.PrintResult { + define: *const Define, + source: *const logger.Source, + comptime Writer: type, + writer: Writer, + ) !usize { var ast: js_ast.Ast = undefined; switch (loader) { @@ -704,7 +796,8 @@ pub const Transformer = struct { var symbols: [][]js_ast.Symbol = &([_][]js_ast.Symbol{ast.symbols}); return try js_printer.printAst( - result_allocator, + Writer, + writer, ast, js_ast.Symbol.Map.initList(symbols), source, |