diff options
author | 2023-05-14 06:13:39 -0700 | |
---|---|---|
committer | 2023-05-14 06:13:39 -0700 | |
commit | 893f70fee4a62b7729abc17257aee89a2dce0069 (patch) | |
tree | 9a1da782efdadc8ab9869293f6fd9cd64e5b4ba7 | |
parent | 7f25aa9e0864e95aad72ee85d475a03aee68bfb4 (diff) | |
download | bun-893f70fee4a62b7729abc17257aee89a2dce0069.tar.gz bun-893f70fee4a62b7729abc17257aee89a2dce0069.tar.zst bun-893f70fee4a62b7729abc17257aee89a2dce0069.zip |
Single-file standalone Bun executables (#2879)
* Add LIEF
* Compile LIEF
* Implement support for embedding files on macOS
* proof of concept
* Add zstd
* Implement runtime support
* Move some code around
* Update .gitmodules
* Upgrade zig
https://github.com/ziglang/zig/pull/15278
* leftover
* leftover
* delete dead code
* Fix extname
* Revert "Upgrade zig"
This reverts commit dd968f30bffb6c06e34302645a3a4468c957fb4e.
* Revert "leftover"
This reverts commit 7664de7686276cfba431103847d35b9270433dee.
* Revert "leftover"
This reverts commit 498005be06a8a1747d48824310e5a020b1f90d97.
* various fixes
* it works!
* leftover
* Make `zig build` a little faster
* give up on code signing support
* Support Linux & macOS
* Finish removing LIEF
* few more
* Add zstd to list of deps
* make it pretty
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r-- | .gitmodules | 4 | ||||
-rw-r--r-- | .vscode/settings.json | 14 | ||||
-rw-r--r-- | Dockerfile | 28 | ||||
-rw-r--r-- | Makefile | 9 | ||||
-rw-r--r-- | build.zig | 136 | ||||
-rw-r--r-- | docs/bundler/migration.md | 2 | ||||
-rw-r--r-- | docs/project/licensing.md | 5 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 88 | ||||
-rw-r--r-- | src/bun.js/module_loader.zig | 10 | ||||
-rw-r--r-- | src/bun.zig | 6 | ||||
-rw-r--r-- | src/bun_js.zig | 83 | ||||
-rw-r--r-- | src/bundler/bundle_v2.zig | 37 | ||||
-rw-r--r-- | src/cli.zig | 70 | ||||
-rw-r--r-- | src/cli/build_command.zig | 197 | ||||
-rw-r--r-- | src/darwin_c.zig | 6 | ||||
m--------- | src/deps/zstd | 0 | ||||
-rw-r--r-- | src/deps/zstd.zig | 227 | ||||
-rw-r--r-- | src/mdx/mdx_parser.zig | 1835 | ||||
-rw-r--r-- | src/options.zig | 3 | ||||
-rw-r--r-- | src/output.zig | 16 | ||||
-rw-r--r-- | src/resolver/resolver.zig | 23 | ||||
-rw-r--r-- | src/runtime.zig | 17 | ||||
-rw-r--r-- | src/sourcemap/sourcemap.zig | 76 | ||||
-rw-r--r-- | src/standalone_bun.zig | 480 | ||||
-rw-r--r-- | src/string_builder.zig | 57 | ||||
-rw-r--r-- | src/which.zig | 6 | ||||
-rw-r--r-- | test/bundler/bundler_edgecase.test.ts | 2 | ||||
-rw-r--r-- | test/bundler/expectBundled.ts | 2 |
28 files changed, 1415 insertions, 2024 deletions
diff --git a/.gitmodules b/.gitmodules index c22446cbd..3f6263101 100644 --- a/.gitmodules +++ b/.gitmodules @@ -65,3 +65,7 @@ fetchRecurseSubmodules = false [submodule "src/deps/c-ares"] path = src/deps/c-ares url = https://github.com/c-ares/c-ares.git +[submodule "src/deps/zstd"] + path = src/deps/zstd + url = git@github.com:facebook/zstd.git + ignore = dirty
\ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e3320ca3..6c8a24381 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -77,6 +77,7 @@ "src/deps/lol-html": true, "src/deps/c-ares": true, "src/deps/tinycc": true, + "src/deps/zstd": true, "test/snippets/package-json-exports/_node_modules_copy": true }, "C_Cpp.files.exclude": { @@ -204,7 +205,18 @@ "compare": "cpp", "concepts": "cpp", "typeindex": "cpp", - "__verbose_abort": "cpp" + "__verbose_abort": "cpp", + "__std_stream": "cpp", + "any": "cpp", + "charconv": "cpp", + "csignal": "cpp", + "format": "cpp", + "forward_list": "cpp", + "future": "cpp", + "regex": "cpp", + "span": "cpp", + "valarray": "cpp", + "codecvt": "cpp" }, "cmake.configureOnOpen": false, "C_Cpp.errorSquiggles": "enabled", diff --git a/Dockerfile b/Dockerfile index 6479c209c..dcce1ca4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -503,6 +503,33 @@ ENV LIB_ICU_PATH=${WEBKIT_DIR}/lib RUN --mount=type=cache,target=/ccache cd $BUN_DIR && make sqlite +FROM bun-base as zstd + +ARG DEBIAN_FRONTEND +ARG GITHUB_WORKSPACE +ARG ZIG_PATH +# Directory extracts to "bun-webkit" +ARG WEBKIT_DIR +ARG BUN_RELEASE_DIR +ARG BUN_DEPS_OUT_DIR +ARG BUN_DIR + +ARG CPU_TARGET +ENV CPU_TARGET=${CPU_TARGET} + +ENV CCACHE_DIR=/ccache + +COPY Makefile ${BUN_DIR}/Makefile +COPY src/deps/zstd ${BUN_DIR}/src/deps/zstd +COPY .prettierrc.cjs ${BUN_DIR}/.prettierrc.cjs + +WORKDIR $BUN_DIR + +ENV JSC_BASE_DIR=${WEBKIT_DIR} +ENV LIB_ICU_PATH=${WEBKIT_DIR}/lib + +RUN --mount=type=cache,target=/ccache cd $BUN_DIR && make zstd + FROM scratch as build_release_cpp COPY --from=compile_cpp /tmp/*.o / @@ -535,6 +562,7 @@ COPY --from=lolhtml ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=mimalloc ${BUN_DEPS_OUT_DIR}/*.o ${BUN_DEPS_OUT_DIR}/ COPY --from=picohttp ${BUN_DEPS_OUT_DIR}/*.o ${BUN_DEPS_OUT_DIR}/ COPY --from=sqlite ${BUN_DEPS_OUT_DIR}/*.o ${BUN_DEPS_OUT_DIR}/ +COPY --from=zstd ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=tinycc ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=uws ${BUN_DEPS_OUT_DIR}/*.a ${BUN_DEPS_OUT_DIR}/ COPY --from=uws ${BUN_DEPS_OUT_DIR}/*.o ${BUN_DEPS_OUT_DIR}/ @@ -344,7 +344,7 @@ LINUX_INCLUDE_DIRS := $(ALL_JSC_INCLUDE_DIRS) \ UWS_INCLUDE_DIR := -I$(BUN_DEPS_DIR)/uws/uSockets/src -I$(BUN_DEPS_DIR)/uws/src -I$(BUN_DEPS_DIR) -INCLUDE_DIRS := $(UWS_INCLUDE_DIR) -I$(BUN_DEPS_DIR)/mimalloc/include -Isrc/napi -I$(BUN_DEPS_DIR)/boringssl/include -I$(BUN_DEPS_DIR)/c-ares/include +INCLUDE_DIRS := $(UWS_INCLUDE_DIR) -I$(BUN_DEPS_DIR)/mimalloc/include -I$(BUN_DEPS_DIR)/zstd/include -Isrc/napi -I$(BUN_DEPS_DIR)/boringssl/include -I$(BUN_DEPS_DIR)/c-ares/include ifeq ($(OS_NAME),linux) @@ -452,6 +452,7 @@ ARCHIVE_FILES_WITHOUT_LIBCRYPTO = $(MINIMUM_ARCHIVE_FILES) \ -ltcc \ -lusockets \ -lcares \ + -lzstd \ $(BUN_DEPS_OUT_DIR)/libuwsockets.o ARCHIVE_FILES = $(ARCHIVE_FILES_WITHOUT_LIBCRYPTO) @@ -636,6 +637,9 @@ compile-ffi-test: sqlite: +.PHONY: zstd +zstd: + cd $(BUN_DEPS_DIR)/zstd && rm -rf build-cmake-debug && cmake $(CMAKE_FLAGS) -DZSTD_BUILD_STATIC=ON -B build-cmake-debug -S build/cmake -G Ninja && ninja -C build-cmake-debug && cp build-cmake-debug/lib/libzstd.a $(BUN_DEPS_OUT_DIR)/libzstd.a .PHONY: libarchive libarchive: @@ -908,7 +912,6 @@ bun-codesign-release-local: bun-codesign-release-local-debug: - .PHONY: jsc jsc: jsc-build jsc-copy-headers jsc-bindings .PHONY: jsc-build @@ -1857,7 +1860,7 @@ cold-jsc-start: misctools/cold-jsc-start.cpp -o cold-jsc-start .PHONY: vendor-without-npm -vendor-without-npm: node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp zlib boringssl libarchive lolhtml sqlite usockets uws tinycc c-ares +vendor-without-npm: node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp zlib boringssl libarchive lolhtml sqlite usockets uws tinycc c-ares zstd .PHONY: vendor-without-check vendor-without-check: npm-install vendor-without-npm @@ -71,6 +71,24 @@ const BunBuildOptions = struct { sizegen: bool = false, base_path: [:0]const u8 = "", + runtime_js_version: u64 = 0, + fallback_html_version: u64 = 0, + + pub fn updateRuntime(this: *BunBuildOptions) anyerror!void { + var runtime_out_file = try std.fs.cwd().openFile("src/runtime.out.js", .{ .mode = .read_only }); + const runtime_hash = std.hash.Wyhash.hash( + 0, + try runtime_out_file.readToEndAlloc(std.heap.page_allocator, try runtime_out_file.getEndPos()), + ); + this.runtime_js_version = runtime_hash; + var fallback_out_file = try std.fs.cwd().openFile("src/fallback.out.js", .{ .mode = .read_only }); + const fallback_hash = std.hash.Wyhash.hash( + 0, + try fallback_out_file.readToEndAlloc(std.heap.page_allocator, try fallback_out_file.getEndPos()), + ); + this.fallback_html_version = fallback_hash; + } + pub fn step(this: BunBuildOptions, b: anytype) *std.build.OptionsStep { var opts = b.addOptions(); opts.addOption(@TypeOf(this.canary), "is_canary", this.canary); @@ -79,6 +97,8 @@ const BunBuildOptions = struct { opts.addOption(@TypeOf(this.bindgen), "bindgen", this.bindgen); opts.addOption(@TypeOf(this.sizegen), "sizegen", this.sizegen); opts.addOption(@TypeOf(this.base_path), "base_path", this.base_path); + opts.addOption(@TypeOf(this.runtime_js_version), "runtime_js_version", this.runtime_js_version); + opts.addOption(@TypeOf(this.fallback_html_version), "fallback_html_version", this.fallback_html_version); return opts; } }; @@ -105,28 +125,6 @@ const fmt = struct { } }; -fn updateRuntime() anyerror!void { - var runtime_out_file = try std.fs.cwd().openFile("src/runtime.out.js", .{ .mode = .read_only }); - const runtime_hash = std.hash.Wyhash.hash( - 0, - try runtime_out_file.readToEndAlloc(std.heap.page_allocator, try runtime_out_file.getEndPos()), - ); - const runtime_version_file = std.fs.cwd().createFile("src/runtime.version", .{ .truncate = true }) catch std.debug.panic("Failed to create src/runtime.version", .{}); - defer runtime_version_file.close(); - runtime_version_file.writer().print("{any}", .{fmt.hexInt(runtime_hash)}) catch unreachable; - var fallback_out_file = try std.fs.cwd().openFile("src/fallback.out.js", .{ .mode = .read_only }); - const fallback_hash = std.hash.Wyhash.hash( - 0, - try fallback_out_file.readToEndAlloc(std.heap.page_allocator, try fallback_out_file.getEndPos()), - ); - - const fallback_version_file = std.fs.cwd().createFile("src/fallback.version", .{ .truncate = true }) catch std.debug.panic("Failed to create src/fallback.version", .{}); - - fallback_version_file.writer().print("{any}", .{fmt.hexInt(fallback_hash)}) catch unreachable; - - fallback_version_file.close(); -} - var x64 = "x64"; var optimize: std.builtin.OptimizeMode = undefined; @@ -194,8 +192,6 @@ pub fn build(b: *Build) !void { else "root.zig"; - updateRuntime() catch {}; - const min_version: std.builtin.Version = if (target.getOsTag() != .freestanding) target.getOsVersionMin().semver else @@ -271,6 +267,8 @@ pub fn build(b: *Build) !void { obj.target.cpu_model = .{ .explicit = &std.Target.aarch64.cpu.generic }; } + try default_build_options.updateRuntime(); + // we have to dump to stderr because stdout is read by zls std.io.getStdErr().writer().print("Build {s} v{} - v{} ({s})\n", .{ triplet, @@ -454,38 +452,9 @@ pub fn build(b: *Build) !void { } try configureObjectStep(b, headers_obj, @TypeOf(target), target, obj.main_pkg_path.?); - try linkObjectFiles(b, headers_obj, target); headers_step.dependOn(&headers_obj.step); headers_obj.addOptions("build_options", default_build_options.step(b)); - - // var iter = headers_obj.modules.iterator(); - // while (iter.next()) |item| { - // const module = @ptrCast(*Module, item.value_ptr); - // } - // // while (headers_obj.modules.) - // for (headers_obj.packages.items) |pkg_| { - // const pkg: std.build.Pkg = pkg_; - // if (std.mem.eql(u8, pkg.name, "clap")) continue; - // var test_ = b.addTestSource(pkg.source); - - // b - // .test_.setMainPkgPath(obj.main_pkg_path.?); - // try configureObjectStep(b, test_, @TypeOf(target), target, obj.main_pkg_path.?); - // try linkObjectFiles(b, test_, target); - // test_.addOptions("build_options", default_build_options.step(b)); - - // if (pkg.dependencies) |children| { - // test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator); - // try test_.packages.appendSlice(children); - // } - - // var before = b.addLog("\x1b[" ++ color_map.get("magenta").? ++ "\x1b[" ++ color_map.get("b").? ++ "[{s} tests]" ++ "\x1b[" ++ color_map.get("d").? ++ " ----\n\n" ++ "\x1b[0m", .{pkg.name}); - // var after = b.addLog("\x1b[" ++ color_map.get("d").? ++ "–––---\n\n" ++ "\x1b[0m", .{}); - // headers_step.dependOn(&before.step); - // headers_step.dependOn(&test_.step); - // headers_step.dependOn(&after.step); - // } } b.default_step.dependOn(obj_step); @@ -493,67 +462,6 @@ pub fn build(b: *Build) !void { pub var original_make_fn: ?*const fn (step: *std.build.Step) anyerror!void = null; -// Due to limitations in std.build.Builder -// we cannot use this with debugging -// so I am leaving this here for now, with the eventual intent to switch to std.build.Builder -// but it is dead code -pub fn linkObjectFiles(b: *Build, obj: *CompileStep, target: anytype) !void { - if (target.getOsTag() == .freestanding) - return; - var dirs_to_search = std.BoundedArray([]const u8, 32).init(0) catch unreachable; - const arm_brew_prefix: []const u8 = "/opt/homebrew"; - const x86_brew_prefix: []const u8 = "/usr/local"; - try dirs_to_search.append(b.env_map.get("BUN_DEPS_OUT_DIR") orelse b.env_map.get("BUN_DEPS_DIR") orelse @as([]const u8, b.pathFromRoot("src/deps"))); - if (target.getOsTag() == .macos) { - if (target.getCpuArch().isAARCH64()) { - try dirs_to_search.append(comptime arm_brew_prefix ++ "/opt/icu4c/lib/"); - } else { - try dirs_to_search.append(comptime x86_brew_prefix ++ "/opt/icu4c/lib/"); - } - } - - if (b.env_map.get("JSC_LIB")) |jsc| { - try dirs_to_search.append(jsc); - } - - var added = std.AutoHashMap(u64, void).init(b.allocator); - - const files_we_care_about = std.ComptimeStringMap([]const u8, .{ - .{ "libmimalloc.o", "libmimalloc.o" }, - .{ "libz.a", "libz.a" }, - .{ "libarchive.a", "libarchive.a" }, - .{ "libssl.a", "libssl.a" }, - .{ "picohttpparser.o", "picohttpparser.o" }, - .{ "libcrypto.boring.a", "libcrypto.boring.a" }, - .{ "libicuuc.a", "libicuuc.a" }, - .{ "libicudata.a", "libicudata.a" }, - .{ "libicui18n.a", "libicui18n.a" }, - .{ "libJavaScriptCore.a", "libJavaScriptCore.a" }, - .{ "libWTF.a", "libWTF.a" }, - .{ "libbmalloc.a", "libbmalloc.a" }, - .{ "liblolhtml.a", "liblolhtml.a" }, - .{ "uSockets.a", "uSockets.a" }, - }); - - for (dirs_to_search.slice()) |deps_path| { - var deps_dir = std.fs.cwd().openIterableDir(deps_path, .{}) catch continue; - var iterator = deps_dir.iterate(); - obj.addIncludePath(deps_path); - obj.addLibraryPath(deps_path); - - while (iterator.next() catch null) |entr| { - const entry: std.fs.IterableDir.Entry = entr; - if (files_we_care_about.get(entry.name)) |obj_name| { - var has_added = try added.getOrPut(std.hash.Wyhash.hash(0, obj_name)); - if (!has_added.found_existing) { - var paths = [_][]const u8{ deps_path, obj_name }; - obj.addObjectFile(try std.fs.path.join(b.allocator, &paths)); - } - } - } - } -} - pub fn configureObjectStep(b: *std.build.Builder, obj: *CompileStep, comptime Target: type, target: Target, main_pkg_path: []const u8) !void { obj.setMainPkgPath(main_pkg_path); diff --git a/docs/bundler/migration.md b/docs/bundler/migration.md index 7b375fda5..e76d50103 100644 --- a/docs/bundler/migration.md +++ b/docs/bundler/migration.md @@ -35,7 +35,7 @@ In Bun's CLI, simple boolean flags like `--minify` do not accept an argument. Ot - `--bundle` - n/a -- Bun always bundles, use `--transpile` to disable this behavior. +- Bun always bundles, use `--no-bundle` to disable this behavior. --- diff --git a/docs/project/licensing.md b/docs/project/licensing.md index ef88d0674..ea49acb1d 100644 --- a/docs/project/licensing.md +++ b/docs/project/licensing.md @@ -50,6 +50,11 @@ Bun statically links these libraries: --- +- [`zstd`](https://github.com/facebook/zstd) +- dual-licensed under the BSD License or GPLv2 license + +--- + - [`simdutf`](https://github.com/simdutf/simdutf) - Apache 2.0 diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 8bf13314b..34825c3e3 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -361,6 +361,7 @@ pub const VirtualMachine = struct { pending_unref_counter: i32 = 0, preload: []const string = &[_][]const u8{}, unhandled_pending_rejection_to_capture: ?*JSC.JSValue = null, + standalone_module_graph: ?*bun.StandaloneModuleGraph = null, hot_reload: bun.CLI.Command.HotReload = .none, @@ -723,6 +724,92 @@ pub const VirtualMachine = struct { return VMHolder.vm != null; } + pub fn initWithModuleGraph( + allocator: std.mem.Allocator, + log: *logger.Log, + graph: *bun.StandaloneModuleGraph, + ) !*VirtualMachine { + VMHolder.vm = try allocator.create(VirtualMachine); + var console = try allocator.create(ZigConsoleClient); + console.* = ZigConsoleClient.init(Output.errorWriter(), Output.writer()); + const bundler = try Bundler.init( + allocator, + log, + std.mem.zeroes(Api.TransformOptions), + null, + null, + ); + var vm = VMHolder.vm.?; + + vm.* = VirtualMachine{ + .global = undefined, + .allocator = allocator, + .entry_point = ServerEntryPoint{}, + .event_listeners = EventListenerMixin.Map.init(allocator), + .bundler = bundler, + .console = console, + .node_modules = bundler.options.node_modules_bundle, + .log = log, + .flush_list = std.ArrayList(string).init(allocator), + .blobs = null, + .origin = bundler.options.origin, + .saved_source_map_table = SavedSourceMap.HashTable.init(allocator), + .source_mappings = undefined, + .macros = MacroMap.init(allocator), + .macro_entry_points = @TypeOf(vm.macro_entry_points).init(allocator), + .origin_timer = std.time.Timer.start() catch @panic("Please don't mess with timers."), + .origin_timestamp = getOriginTimestamp(), + .ref_strings = JSC.RefString.Map.init(allocator), + .file_blobs = JSC.WebCore.Blob.Store.Map.init(allocator), + .standalone_module_graph = graph, + }; + vm.source_mappings = .{ .map = &vm.saved_source_map_table }; + vm.regular_event_loop.tasks = EventLoop.Queue.init( + default_allocator, + ); + vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; + vm.regular_event_loop.concurrent_tasks = .{}; + vm.event_loop = &vm.regular_event_loop; + + vm.bundler.macro_context = null; + vm.bundler.resolver.store_fd = false; + + vm.bundler.resolver.onWakePackageManager = .{ + .context = &vm.modules, + .handler = ModuleLoader.AsyncModule.Queue.onWakeHandler, + .onDependencyError = JSC.ModuleLoader.AsyncModule.Queue.onDependencyError, + }; + + vm.bundler.resolver.standalone_module_graph = graph; + + // Avoid reading from tsconfig.json & package.json when we're in standalone mode + vm.bundler.configureLinkerWithAutoJSX(false); + try vm.bundler.configureFramework(false); + + vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler); + + var global_classes: [GlobalClasses.len]js.JSClassRef = undefined; + inline for (GlobalClasses, 0..) |Class, i| { + global_classes[i] = Class.get().*; + } + vm.global = ZigGlobalObject.create( + &global_classes, + @intCast(i32, global_classes.len), + vm.console, + ); + vm.regular_event_loop.global = vm.global; + vm.regular_event_loop.virtual_machine = vm; + + if (source_code_printer == null) { + var writer = try js_printer.BufferWriter.init(allocator); + source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable; + source_code_printer.?.* = js_printer.BufferPrinter.init(writer); + source_code_printer.?.ctx.append_null_byte = false; + } + + return vm; + } + pub fn init( allocator: std.mem.Allocator, _args: Api.TransformOptions, @@ -1196,6 +1283,7 @@ pub const VirtualMachine = struct { res.* = ErrorableZigString.ok(ZigString.init(hardcoded.path)); return; } + var old_log = jsc_vm.log; var log = logger.Log.init(jsc_vm.allocator); defer log.deinit(); diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 131ac5b59..6524c8084 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -2077,6 +2077,16 @@ pub const ModuleLoader = struct { .source_url = ZigString.init(specifier), .hash = 0, }; + } else if (jsc_vm.standalone_module_graph) |graph| { + if (graph.files.get(specifier)) |file| { + return ResolvedSource{ + .allocator = null, + .source_code = ZigString.init(file.contents), + .specifier = ZigString.init(specifier), + .source_url = ZigString.init(specifier), + .hash = 0, + }; + } } return null; diff --git a/src/bun.zig b/src/bun.zig index 6b8dffbfa..1520a179e 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -161,7 +161,7 @@ pub const fmt = struct { try fmt.formatFloatDecimal(new_value / 1000.0, .{ .precision = 2 }, writer); return writer.writeAll(" KB"); } else { - try fmt.formatFloatDecimal(new_value, .{ .precision = if (std.math.approxEqAbs(f64, new_value, @trunc(new_value), 0.100)) @as(usize, 0) else @as(usize, 2) }, writer); + try fmt.formatFloatDecimal(new_value, .{ .precision = if (std.math.approxEqAbs(f64, new_value, @trunc(new_value), 0.100)) @as(usize, 1) else @as(usize, 2) }, writer); } const buf = switch (1000) { @@ -1509,3 +1509,7 @@ pub fn openFileForPath(path_: [:0]const u8) !std.fs.File { } pub const Generation = u16; + +pub const zstd = @import("./deps/zstd.zig"); +pub const StringPointer = Schema.Api.StringPointer; +pub const StandaloneModuleGraph = @import("./standalone_bun.zig").StandaloneModuleGraph; diff --git a/src/bun_js.zig b/src/bun_js.zig index 5b1f73386..5a4eb4f8a 100644 --- a/src/bun_js.zig +++ b/src/bun_js.zig @@ -37,14 +37,94 @@ const VirtualMachine = JSC.VirtualMachine; var run: Run = undefined; pub const Run = struct { - file: std.fs.File, ctx: Command.Context, vm: *VirtualMachine, entry_path: string, arena: Arena = undefined, any_unhandled: bool = false, + pub fn bootStandalone(ctx_: Command.Context, entry_path: string, graph: bun.StandaloneModuleGraph) !void { + var ctx = ctx_; + JSC.markBinding(@src()); + bun.JSC.initialize(); + + var graph_ptr = try bun.default_allocator.create(bun.StandaloneModuleGraph); + graph_ptr.* = graph; + + js_ast.Expr.Data.Store.create(default_allocator); + js_ast.Stmt.Data.Store.create(default_allocator); + var arena = try Arena.init(); + + if (!ctx.debug.loaded_bunfig) { + try bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", &ctx, .RunCommand); + } + + run = .{ + .vm = try VirtualMachine.initWithModuleGraph(arena.allocator(), ctx.log, graph_ptr), + .arena = arena, + .ctx = ctx, + .entry_path = entry_path, + }; + + var vm = run.vm; + var b = &vm.bundler; + vm.preload = ctx.preloads; + vm.argv = ctx.passthrough; + vm.arena = &run.arena; + vm.allocator = arena.allocator(); + + b.options.install = ctx.install; + b.resolver.opts.install = ctx.install; + b.resolver.opts.global_cache = ctx.debug.global_cache; + b.resolver.opts.prefer_offline_install = (ctx.debug.offline_mode_setting orelse .online) == .offline; + b.resolver.opts.prefer_latest_install = (ctx.debug.offline_mode_setting orelse .online) == .latest; + b.options.global_cache = b.resolver.opts.global_cache; + b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install; + b.options.prefer_latest_install = b.resolver.opts.prefer_latest_install; + b.resolver.env_loader = b.env; + + b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; + b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; + b.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; + b.resolver.opts.minify_whitespace = ctx.bundler_options.minify_whitespace; + + // b.options.minify_syntax = ctx.bundler_options.minify_syntax; + + if (ctx.debug.macros) |macros| { + b.options.macro_remap = macros; + } + + b.configureRouter(false) catch { + if (Output.enable_ansi_colors_stderr) { + vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; + } else { + vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {}; + } + Output.prettyErrorln("\n", .{}); + Global.exit(1); + }; + b.configureDefines() catch { + if (Output.enable_ansi_colors_stderr) { + vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {}; + } else { + vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {}; + } + Output.prettyErrorln("\n", .{}); + Global.exit(1); + }; + + AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env); + + vm.loadExtraEnv(); + vm.is_main_thread = true; + JSC.VirtualMachine.is_main_thread_vm = true; + + var callback = OpaqueWrap(Run, Run.start); + vm.global.vm().holdAPILock(&run, callback); + } + pub fn boot(ctx_: Command.Context, file: std.fs.File, entry_path: string) !void { + _ = file; var ctx = ctx_; JSC.markBinding(@src()); bun.JSC.initialize(); @@ -66,7 +146,6 @@ pub const Run = struct { null, ctx.debug.hot_reload != .none, ), - .file = file, .arena = arena, .ctx = ctx, .entry_path = entry_path, diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 57adc904a..34d2780de 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -332,6 +332,7 @@ pub const BundleV2 = struct { bun_watcher: ?*Watcher.Watcher = null, plugins: ?*JSC.API.JSBundler.Plugin = null, completion: ?*JSBundleCompletionTask = null, + source_code_length: usize = 0, // There is a race condition where an onResolve plugin may schedule a task on the bundle thread before it's parsing task completes resolve_tasks_waiting_for_import_source_index: std.AutoArrayHashMapUnmanaged(Index.Int, BabyList(struct { to_source_index: Index, import_record_index: u32 })) = .{}, @@ -993,6 +994,9 @@ pub const BundleV2 = struct { event_loop: EventLoop, unique_key: u64, enable_reloading: bool, + reachable_files_count: *usize, + minify_duration: *u64, + source_code_size: *u64, ) !std.ArrayList(options.OutputFile) { var this = try BundleV2.init(bundler, allocator, event_loop, enable_reloading, null, null); this.unique_key = unique_key; @@ -1009,6 +1013,9 @@ pub const BundleV2 = struct { this.waitForParse(); + minify_duration.* = @intCast(u64, @divTrunc(@truncate(i64, std.time.nanoTimestamp()) - @truncate(i64, bun.CLI.start_time), @as(i64, std.time.ns_per_ms))); + source_code_size.* = this.source_code_length; + if (this.graph.use_directive_entry_points.len > 0) { if (this.bundler.log.msgs.items.len > 0) { return error.BuildFailed; @@ -1023,6 +1030,7 @@ pub const BundleV2 = struct { } const reachable_files = try this.findReachableFiles(); + reachable_files_count.* = reachable_files.len -| 1; // - 1 for the runtime try this.processFilesToCopy(reachable_files); @@ -2098,6 +2106,10 @@ pub const BundleV2 = struct { // Warning: this array may resize in this function call // do not reuse it. graph.input_files.items(.source)[result.source.index.get()] = result.source; + this.source_code_length += if (!result.source.index.isRuntime()) + result.source.contents.len + else + @as(usize, 0); graph.input_files.items(.unique_key_for_additional_file)[result.source.index.get()] = result.unique_key_for_additional_file; graph.input_files.items(.content_hash_for_additional_file)[result.source.index.get()] = result.content_hash_for_additional_file; @@ -8834,20 +8846,25 @@ const LinkerContext = struct { if (root_path.len > 0) { try c.writeOutputFilesToDisk(root_path, chunks, react_client_components_manifest, &output_files); } else { + // In-memory build for (chunks) |*chunk| { + var display_size: usize = 0; + const _code_result = if (c.options.source_maps != .none) chunk.intermediate_output.codeWithSourceMapShifts( null, c.parse_graph, c.resolver.opts.public_path, chunk, chunks, + &display_size, ) else chunk.intermediate_output.code( null, c.parse_graph, c.resolver.opts.public_path, chunk, chunks, + &display_size, ); var code_result = _code_result catch @panic("Failed to allocate memory for output file"); @@ -8918,6 +8935,7 @@ const LinkerContext = struct { .hash = chunk.isolated_hash, .loader = .js, .input_path = input_path, + .display_size = @truncate(u32, display_size), .output_kind = if (chunk.entry_point.is_entry_point) c.graph.files.items(.entry_point_kind)[chunk.entry_point.source_index].OutputKind() else @@ -9014,7 +9032,7 @@ const LinkerContext = struct { }; } } - + var display_size: usize = 0; const _code_result = if (c.options.source_maps != .none) chunk.intermediate_output.codeWithSourceMapShifts( code_allocator, @@ -9022,6 +9040,7 @@ const LinkerContext = struct { c.resolver.opts.public_path, chunk, chunks, + &display_size, ) else chunk.intermediate_output.code( @@ -9030,6 +9049,7 @@ const LinkerContext = struct { c.resolver.opts.public_path, chunk, chunks, + &display_size, ); var code_result = _code_result catch @panic("Failed to allocate memory for output chunk"); @@ -9169,6 +9189,7 @@ const LinkerContext = struct { else null, .size = @truncate(u32, code_result.buffer.len), + .display_size = @truncate(u32, display_size), .data = .{ .saved = 0, }, @@ -10635,6 +10656,7 @@ pub const Chunk = struct { import_prefix: []const u8, chunk: *Chunk, chunks: []Chunk, + display_size: ?*usize, ) !CodeResult { const additional_files = graph.input_files.items(.additional_files); const unique_key_for_additional_files = graph.input_files.items(.unique_key_for_additional_file); @@ -10678,6 +10700,10 @@ pub const Chunk = struct { } } + if (display_size) |amt| { + amt.* = count; + } + const debug_id_len = if (comptime FeatureFlags.source_map_debug_id) std.fmt.count("\n//# debugId={}\n", .{bun.sourcemap.DebugIDFormatter{ .id = chunk.isolated_hash }}) else @@ -10767,6 +10793,10 @@ pub const Chunk = struct { const allocator = allocator_to_use orelse allocatorForSize(joiny.len); + if (display_size) |amt| { + amt.* = joiny.len; + } + const buffer = brk: { if (comptime FeatureFlags.source_map_debug_id) { // This comment must go before the //# sourceMappingURL comment @@ -10801,6 +10831,7 @@ pub const Chunk = struct { import_prefix: []const u8, chunk: *Chunk, chunks: []Chunk, + display_size: *usize, ) !CodeResult { const additional_files = graph.input_files.items(.additional_files); switch (this) { @@ -10837,6 +10868,7 @@ pub const Chunk = struct { } } + display_size.* = count; var total_buf = try (allocator_to_use orelse allocatorForSize(count)).alloc(u8, count); var remain = total_buf; @@ -10890,6 +10922,9 @@ pub const Chunk = struct { .joiner => |joiner_| { // TODO: make this safe var joiny = joiner_; + + display_size.* = joiny.len; + return .{ .buffer = try joiny.done((allocator_to_use orelse allocatorForSize(joiny.len))), .shifts = &[_]sourcemap.SourceMapShifts{}, diff --git a/src/cli.zig b/src/cli.zig index ca8208aa2..621f54fd2 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -89,6 +89,7 @@ fn invalidTarget(diag: *clap.Diagnostic, _target: []const u8) noreturn { diag.report(Output.errorWriter(), error.InvalidTarget) catch {}; std.process.exit(1); } + pub const Arguments = struct { pub fn loader_resolver(in: string) !Api.Loader { const option_loader = options.Loader.fromString(in) orelse return error.InvalidLoader; @@ -198,14 +199,14 @@ pub const Arguments = struct { clap.parseParam("--outfile <STR> Write to a file") catch unreachable, clap.parseParam("--root <STR> Root directory used for multiple entry points") catch unreachable, clap.parseParam("--splitting Enable code splitting") catch unreachable, - // clap.parseParam("--manifest <STR> Write JSON manifest") catch unreachable, - // clap.parseParam("--public-path <STR> A prefix to be appended to any import paths in bundled code") catch unreachable, + clap.parseParam("--public-path <STR> A prefix to be appended to any import paths in bundled code") catch unreachable, clap.parseParam("--sourcemap <STR>? Build with sourcemaps - 'inline', 'external', or 'none'") catch unreachable, clap.parseParam("--entry-naming <STR> Customize entry point filenames. Defaults to \"[dir]/[name].[ext]\"") catch unreachable, clap.parseParam("--chunk-naming <STR> Customize chunk filenames. Defaults to \"[name]-[hash].[ext]\"") catch unreachable, clap.parseParam("--asset-naming <STR> Customize asset filenames. Defaults to \"[name]-[hash].[ext]\"") catch unreachable, clap.parseParam("--server-components Enable React Server Components (experimental)") catch unreachable, - clap.parseParam("--transpile Transpile file only, do not bundle") catch unreachable, + clap.parseParam("--no-bundle Transpile file only, do not bundle") catch unreachable, + clap.parseParam("--compile Generate a standalone Bun executable containing your bundled code") catch unreachable, }; // TODO: update test completions @@ -478,7 +479,11 @@ pub const Arguments = struct { ctx.bundler_options.minify_identifiers = minify_flag or args.flag("--minify-identifiers"); if (cmd == .BuildCommand) { - ctx.bundler_options.transform_only = args.flag("--transpile"); + ctx.bundler_options.transform_only = args.flag("--no-bundle"); + + if (args.flag("--compile")) { + ctx.bundler_options.compile = true; + } if (args.option("--outdir")) |outdir| { if (outdir.len > 0) { @@ -555,7 +560,14 @@ pub const Arguments = struct { entry_points[0], "build", ) or strings.eqlComptime(entry_points[0], "bun"))) { - entry_points = entry_points[1..]; + var out_entry = entry_points[1..]; + for (entry_points, 0..) |entry, i| { + if (entry.len > 0) { + out_entry = out_entry[i..]; + break; + } + } + entry_points = out_entry; } }, .DevCommand => { @@ -661,34 +673,6 @@ pub const Arguments = struct { opts.resolve = Api.ResolveMode.lazy; - switch (comptime cmd) { - .BuildCommand => { - // if (args.option("--resolve")) |_resolve| { - // switch (ResolveMatcher.match(_resolve)) { - // ResolveMatcher.case("disable") => { - // opts.resolve = Api.ResolveMode.disable; - // }, - // ResolveMatcher.case("bundle") => { - // opts.resolve = Api.ResolveMode.bundle; - // }, - // ResolveMatcher.case("dev") => { - // opts.resolve = Api.ResolveMode.dev; - // }, - // ResolveMatcher.case("lazy") => { - // opts.resolve = Api.ResolveMode.lazy; - // }, - // else => { - // diag.name.long = "--resolve"; - // diag.arg = _resolve; - // try diag.report(Output.errorWriter(), error.InvalidResolveOption); - // std.process.exit(1); - // }, - // } - // } - }, - else => {}, - } - const TargetMatcher = strings.ExactSizeMatcher(8); if (args.option("--target")) |_target| { @@ -941,6 +925,8 @@ pub const Command = struct { has_loaded_global_config: bool = false, pub const BundlerOptions = struct { + compile: bool = false, + outdir: []const u8 = "", outfile: []const u8 = "", root_dir: []const u8 = "", @@ -1119,6 +1105,24 @@ pub const Command = struct { // _ = BunxCommand; } + if (try bun.StandaloneModuleGraph.fromExecutable(bun.default_allocator)) |graph| { + var ctx = Command.Context{ + .args = std.mem.zeroes(Api.TransformOptions), + .log = log, + .start_time = start_time, + .allocator = bun.default_allocator, + }; + + ctx.args.target = Api.Target.bun; + + try @import("./bun_js.zig").Run.bootStandalone( + ctx, + graph.entryPoint().name, + graph, + ); + return; + } + const tag = which(); switch (tag) { diff --git a/src/cli/build_command.zig b/src/cli/build_command.zig index 354c481cc..3d2d9948a 100644 --- a/src/cli/build_command.zig +++ b/src/cli/build_command.zig @@ -36,23 +36,30 @@ var estimated_input_lines_of_code_: usize = undefined; pub const BuildCommand = struct { pub fn exec( - ctx: Command.Context, + ctx_: Command.Context, ) !void { Global.configureAllocator(.{ .long_running = true }); + var ctx = ctx_; var allocator = ctx.allocator; var log = ctx.log; estimated_input_lines_of_code_ = 0; + if (ctx.bundler_options.compile) { + // set this early so that externals are set up correctly and define is right + ctx.args.target = .bun; + } var this_bundler = try bundler.Bundler.init(allocator, log, ctx.args, null, null); this_bundler.options.source_map = options.SourceMapOption.fromApi(ctx.args.source_map); this_bundler.resolver.opts.source_map = options.SourceMapOption.fromApi(ctx.args.source_map); - if (this_bundler.options.source_map == .external and ctx.bundler_options.outdir.len == 0) { + if (this_bundler.options.source_map == .external and ctx.bundler_options.outdir.len == 0 and !ctx.bundler_options.compile) { Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use an external source map without --outdir", .{}); Global.exit(1); return; } + var outfile = ctx.bundler_options.outfile; + this_bundler.options.entry_naming = ctx.bundler_options.entry_naming; this_bundler.options.chunk_naming = ctx.bundler_options.chunk_naming; this_bundler.options.asset_naming = ctx.bundler_options.asset_naming; @@ -75,6 +82,59 @@ pub const BuildCommand = struct { this_bundler.options.minify_identifiers = ctx.bundler_options.minify_identifiers; this_bundler.resolver.opts.minify_identifiers = ctx.bundler_options.minify_identifiers; + if (ctx.bundler_options.compile) { + if (ctx.bundler_options.code_splitting) { + Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with --splitting", .{}); + Global.exit(1); + return; + } + + if (this_bundler.options.entry_points.len > 1) { + Output.prettyErrorln("<r><red>error<r><d>:<r> multiple entry points are not supported with --compile", .{}); + Global.exit(1); + return; + } + + if (ctx.bundler_options.outdir.len > 0) { + Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with --outdir", .{}); + Global.exit(1); + return; + } + + // We never want to hit the filesystem for these files + // This "compiled" protocol is specially handled by the module resolver. + this_bundler.options.public_path = "compiled://root/"; + + if (outfile.len == 0) { + outfile = std.fs.path.basename(this_bundler.options.entry_points[0]); + const ext = std.fs.path.extension(outfile); + if (ext.len > 0) { + outfile = outfile[0 .. outfile.len - ext.len]; + } + + if (strings.eqlComptime(outfile, "index")) { + outfile = std.fs.path.basename(std.fs.path.dirname(this_bundler.options.entry_points[0]) orelse "index"); + } + + if (strings.eqlComptime(outfile, "bun")) { + outfile = std.fs.path.basename(std.fs.path.dirname(this_bundler.options.entry_points[0]) orelse "bun"); + } + } + + // If argv[0] is "bun" or "bunx", we don't check if the binary is standalone + if (strings.eqlComptime(outfile, "bun") or strings.eqlComptime(outfile, "bunx")) { + Output.prettyErrorln("<r><red>error<r><d>:<r> cannot use --compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for --outfile", .{}); + Global.exit(1); + return; + } + + if (ctx.bundler_options.transform_only) { + Output.prettyErrorln("<r><red>error<r><d>:<r> --compile does not support --no-bundle", .{}); + Global.exit(1); + return; + } + } + if (this_bundler.options.entry_points.len > 1 and ctx.bundler_options.outdir.len == 0) { Output.prettyErrorln("error: to use multiple entry points, specify --outdir", .{}); Global.exit(1); @@ -150,6 +210,10 @@ pub const BuildCommand = struct { return; } + var reachable_file_count: usize = 0; + var minify_duration: u64 = 0; + var input_code_length: u64 = 0; + const output_files: []options.OutputFile = brk: { if (ctx.bundler_options.transform_only) { this_bundler.options.import_path_format = .relative; @@ -182,6 +246,9 @@ pub const BuildCommand = struct { bun.JSC.AnyEventLoop.init(ctx.allocator), std.crypto.random.int(u64), ctx.debug.hot_reload == .watch, + &reachable_file_count, + &minify_duration, + &input_code_length, ) catch |err| { if (log.msgs.items.len > 0) { try log.printForLogLevel(Output.errorWriter()); @@ -196,21 +263,24 @@ pub const BuildCommand = struct { }; { + var write_summary = false; { dump: { defer Output.flush(); var writer = Output.writer(); var output_dir = this_bundler.options.output_dir; - if (ctx.bundler_options.outfile.len > 0 and output_files.len == 1 and output_files[0].value == .buffer) { - output_dir = std.fs.path.dirname(ctx.bundler_options.outfile) orelse "."; - output_files[0].path = std.fs.path.basename(ctx.bundler_options.outfile); + if (outfile.len > 0 and output_files.len == 1 and output_files[0].value == .buffer) { + output_dir = std.fs.path.dirname(outfile) orelse "."; + output_files[0].path = std.fs.path.basename(outfile); } - if (ctx.bundler_options.outfile.len == 0 and output_files.len == 1 and ctx.bundler_options.outdir.len == 0) { - // if --transpile is passed, it won't have an output dir - if (output_files[0].value == .buffer) - try writer.writeAll(output_files[0].value.buffer.bytes); - break :dump; + if (!ctx.bundler_options.compile) { + if (outfile.len == 0 and output_files.len == 1 and ctx.bundler_options.outdir.len == 0) { + // if --no-bundle is passed, it won't have an output dir + if (output_files[0].value == .buffer) + try writer.writeAll(output_files[0].value.buffer.bytes); + break :dump; + } } var root_path = output_dir; @@ -237,6 +307,105 @@ pub const BuildCommand = struct { ); } + if (ctx.bundler_options.compile) { + const bundled_end = std.time.nanoTimestamp(); + const minified = this_bundler.options.minify_identifiers or this_bundler.options.minify_whitespace or this_bundler.options.minify_syntax; + const padding_buf = [_]u8{' '} ** 16; + + const bundle_until_now = @divTrunc(@truncate(i64, bundled_end - bun.CLI.start_time), @as(i64, std.time.ns_per_ms)); + + const bundle_elapsed = if (minified) + bundle_until_now - @intCast(i64, @truncate(u63, minify_duration)) + else + bundle_until_now; + + const minified_digit_count: usize = switch (minify_duration) { + 0...9 => 3, + 10...99 => 2, + 100...999 => 1, + 1000...9999 => 0, + else => 0, + }; + if (minified) { + Output.pretty("{s}", .{padding_buf[0..@intCast(usize, minified_digit_count)]}); + Output.printElapsedStdoutTrim(@intToFloat(f64, minify_duration)); + const output_size = brk: { + var total_size: u64 = 0; + for (output_files) |f| { + if (f.loader == .js) { + total_size += f.size_without_sourcemap; + } + } + + break :brk total_size; + }; + // this isn't an exact size + // we may inject sourcemaps or comments or import paths + const delta: i64 = @truncate(i64, @intCast(i65, input_code_length) - @intCast(i65, output_size)); + if (delta > 1024) { + Output.prettyln( + " <green>minify<r> -{} <d>(estimate)<r>", + .{ + bun.fmt.size(@intCast(usize, delta)), + }, + ); + } else if (-delta > 1024) { + Output.prettyln( + " <b>minify<r> +{} <d>(estimate)<r>", + .{ + bun.fmt.size(@intCast(usize, -delta)), + }, + ); + } else { + Output.prettyln(" <b>minify<r>", .{}); + } + } + + const bundle_elapsed_digit_count: usize = switch (bundle_elapsed) { + 0...9 => 3, + 10...99 => 2, + 100...999 => 1, + 1000...9999 => 0, + else => 0, + }; + + Output.pretty("{s}", .{padding_buf[0..@intCast(usize, bundle_elapsed_digit_count)]}); + Output.printElapsedStdoutTrim(@intToFloat(f64, bundle_elapsed)); + Output.prettyln( + " <green>bundle<r> {d} modules", + .{ + reachable_file_count, + }, + ); + + Output.flush(); + try bun.StandaloneModuleGraph.toExecutable( + allocator, + output_files, + root_dir, + this_bundler.options.public_path, + outfile, + ); + const compiled_elapsed = @divTrunc(@truncate(i64, std.time.nanoTimestamp() - bundled_end), @as(i64, std.time.ns_per_ms)); + const compiled_elapsed_digit_count: isize = switch (compiled_elapsed) { + 0...9 => 3, + 10...99 => 2, + 100...999 => 1, + 1000...9999 => 0, + else => 0, + }; + + Output.pretty("{s}", .{padding_buf[0..@intCast(usize, compiled_elapsed_digit_count)]}); + + Output.printElapsedStdoutTrim(@intToFloat(f64, compiled_elapsed)); + + Output.prettyln(" <green>compile<r> <b><blue>{s}<r>", .{ + outfile, + }); + + break :dump; + } + // On posix, file handles automatically close on process exit by the OS // Closing files shows up in profiling. // So don't do that unless we actually need to. @@ -268,6 +437,7 @@ pub const BuildCommand = struct { } } } + try root_dir.dir.writeFile(rel_path, value.bytes); }, .move => |value| { @@ -297,8 +467,15 @@ pub const BuildCommand = struct { try std.fmt.formatFloatDecimal(size, .{ .precision = 2 }, writer); try writer.writeAll(" KB\n"); } + + write_summary = true; + } + if (write_summary) { + Output.printStartEndStdout(bun.CLI.start_time, std.time.nanoTimestamp()); + Output.prettyln(" <green>Build<r>", .{}); } } + try log.printForLogLevel(Output.errorWriter()); exitOrWatch(0, ctx.debug.hot_reload == .watch); } diff --git a/src/darwin_c.zig b/src/darwin_c.zig index ab5fb13f7..bd6c3c9ef 100644 --- a/src/darwin_c.zig +++ b/src/darwin_c.zig @@ -67,11 +67,11 @@ pub const COPYFILE_SKIP = @as(c_int, 1); pub const COPYFILE_QUIT = @as(c_int, 2); // int clonefileat(int src_dirfd, const char * src, int dst_dirfd, const char * dst, int flags); -pub extern "c" fn clonefileat(c_int, [*c]const u8, c_int, [*c]const u8, uint32_t: c_int) c_int; +pub extern "c" fn clonefileat(c_int, [*:0]const u8, c_int, [*:0]const u8, uint32_t: c_int) c_int; // int fclonefileat(int srcfd, int dst_dirfd, const char * dst, int flags); -pub extern "c" fn fclonefileat(c_int, c_int, [*c]const u8, uint32_t: c_int) c_int; +pub extern "c" fn fclonefileat(c_int, c_int, [*:0]const u8, uint32_t: c_int) c_int; // int clonefile(const char * src, const char * dst, int flags); -pub extern "c" fn clonefile([*c]const u8, [*c]const u8, uint32_t: c_int) c_int; +pub extern "c" fn clonefile(src: [*:0]const u8, dest: [*:0]const u8, flags: c_int) c_int; // pub fn stat_absolute(path: [:0]const u8) StatError!Stat { // if (builtin.os.tag == .windows) { diff --git a/src/deps/zstd b/src/deps/zstd new file mode 160000 +Subproject 63779c798237346c2b245c546c40b72a5a5913f diff --git a/src/deps/zstd.zig b/src/deps/zstd.zig new file mode 100644 index 000000000..b8bb7e93b --- /dev/null +++ b/src/deps/zstd.zig @@ -0,0 +1,227 @@ +pub extern fn ZSTD_versionNumber() c_uint; +pub extern fn ZSTD_versionString() [*c]const u8; +pub extern fn ZSTD_compress(dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, compressionLevel: c_int) usize; +pub extern fn ZSTD_decompress(dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, compressedSize: usize) usize; +pub extern fn ZSTD_getFrameContentSize(src: ?*const anyopaque, srcSize: usize) c_ulonglong; +pub extern fn ZSTD_getDecompressedSize(src: ?*const anyopaque, srcSize: usize) c_ulonglong; +pub extern fn ZSTD_findFrameCompressedSize(src: ?*const anyopaque, srcSize: usize) usize; +pub extern fn ZSTD_compressBound(srcSize: usize) usize; +pub extern fn ZSTD_isError(code: usize) c_uint; +pub extern fn ZSTD_getErrorName(code: usize) [*:0]const u8; +pub extern fn ZSTD_minCLevel() c_int; +pub extern fn ZSTD_maxCLevel() c_int; +pub extern fn ZSTD_defaultCLevel() c_int; +pub const struct_ZSTD_CCtx_s = opaque {}; +pub const ZSTD_CCtx = struct_ZSTD_CCtx_s; +pub extern fn ZSTD_createCCtx() ?*ZSTD_CCtx; +pub extern fn ZSTD_freeCCtx(cctx: ?*ZSTD_CCtx) usize; +pub extern fn ZSTD_compressCCtx(cctx: ?*ZSTD_CCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, compressionLevel: c_int) usize; +pub const struct_ZSTD_DCtx_s = opaque {}; +pub const ZSTD_DCtx = struct_ZSTD_DCtx_s; +pub extern fn ZSTD_createDCtx() ?*ZSTD_DCtx; +pub extern fn ZSTD_freeDCtx(dctx: ?*ZSTD_DCtx) usize; +pub extern fn ZSTD_decompressDCtx(dctx: ?*ZSTD_DCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize) usize; +pub const ZSTD_fast: c_int = 1; +pub const ZSTD_dfast: c_int = 2; +pub const ZSTD_greedy: c_int = 3; +pub const ZSTD_lazy: c_int = 4; +pub const ZSTD_lazy2: c_int = 5; +pub const ZSTD_btlazy2: c_int = 6; +pub const ZSTD_btopt: c_int = 7; +pub const ZSTD_btultra: c_int = 8; +pub const ZSTD_btultra2: c_int = 9; +pub const ZSTD_strategy = c_uint; +pub const ZSTD_c_compressionLevel: c_int = 100; +pub const ZSTD_c_windowLog: c_int = 101; +pub const ZSTD_c_hashLog: c_int = 102; +pub const ZSTD_c_chainLog: c_int = 103; +pub const ZSTD_c_searchLog: c_int = 104; +pub const ZSTD_c_minMatch: c_int = 105; +pub const ZSTD_c_targetLength: c_int = 106; +pub const ZSTD_c_strategy: c_int = 107; +pub const ZSTD_c_enableLongDistanceMatching: c_int = 160; +pub const ZSTD_c_ldmHashLog: c_int = 161; +pub const ZSTD_c_ldmMinMatch: c_int = 162; +pub const ZSTD_c_ldmBucketSizeLog: c_int = 163; +pub const ZSTD_c_ldmHashRateLog: c_int = 164; +pub const ZSTD_c_contentSizeFlag: c_int = 200; +pub const ZSTD_c_checksumFlag: c_int = 201; +pub const ZSTD_c_dictIDFlag: c_int = 202; +pub const ZSTD_c_nbWorkers: c_int = 400; +pub const ZSTD_c_jobSize: c_int = 401; +pub const ZSTD_c_overlapLog: c_int = 402; +pub const ZSTD_c_experimentalParam1: c_int = 500; +pub const ZSTD_c_experimentalParam2: c_int = 10; +pub const ZSTD_c_experimentalParam3: c_int = 1000; +pub const ZSTD_c_experimentalParam4: c_int = 1001; +pub const ZSTD_c_experimentalParam5: c_int = 1002; +pub const ZSTD_c_experimentalParam6: c_int = 1003; +pub const ZSTD_c_experimentalParam7: c_int = 1004; +pub const ZSTD_c_experimentalParam8: c_int = 1005; +pub const ZSTD_c_experimentalParam9: c_int = 1006; +pub const ZSTD_c_experimentalParam10: c_int = 1007; +pub const ZSTD_c_experimentalParam11: c_int = 1008; +pub const ZSTD_c_experimentalParam12: c_int = 1009; +pub const ZSTD_c_experimentalParam13: c_int = 1010; +pub const ZSTD_c_experimentalParam14: c_int = 1011; +pub const ZSTD_c_experimentalParam15: c_int = 1012; +pub const ZSTD_c_experimentalParam16: c_int = 1013; +pub const ZSTD_c_experimentalParam17: c_int = 1014; +pub const ZSTD_c_experimentalParam18: c_int = 1015; +pub const ZSTD_c_experimentalParam19: c_int = 1016; +pub const ZSTD_cParameter = c_uint; +pub const ZSTD_bounds = extern struct { + @"error": usize, + lowerBound: c_int, + upperBound: c_int, +}; +pub extern fn ZSTD_cParam_getBounds(cParam: ZSTD_cParameter) ZSTD_bounds; +pub extern fn ZSTD_CCtx_setParameter(cctx: ?*ZSTD_CCtx, param: ZSTD_cParameter, value: c_int) usize; +pub extern fn ZSTD_CCtx_setPledgedSrcSize(cctx: ?*ZSTD_CCtx, pledgedSrcSize: c_ulonglong) usize; +pub const ZSTD_reset_session_only: c_int = 1; +pub const ZSTD_reset_parameters: c_int = 2; +pub const ZSTD_reset_session_and_parameters: c_int = 3; +pub const ZSTD_ResetDirective = c_uint; +pub extern fn ZSTD_CCtx_reset(cctx: ?*ZSTD_CCtx, reset: ZSTD_ResetDirective) usize; +pub extern fn ZSTD_compress2(cctx: ?*ZSTD_CCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize) usize; +pub const ZSTD_d_windowLogMax: c_int = 100; +pub const ZSTD_d_experimentalParam1: c_int = 1000; +pub const ZSTD_d_experimentalParam2: c_int = 1001; +pub const ZSTD_d_experimentalParam3: c_int = 1002; +pub const ZSTD_d_experimentalParam4: c_int = 1003; +pub const ZSTD_d_experimentalParam5: c_int = 1004; +pub const ZSTD_dParameter = c_uint; +pub extern fn ZSTD_dParam_getBounds(dParam: ZSTD_dParameter) ZSTD_bounds; +pub extern fn ZSTD_DCtx_setParameter(dctx: ?*ZSTD_DCtx, param: ZSTD_dParameter, value: c_int) usize; +pub extern fn ZSTD_DCtx_reset(dctx: ?*ZSTD_DCtx, reset: ZSTD_ResetDirective) usize; +pub const struct_ZSTD_inBuffer_s = extern struct { + src: ?*const anyopaque, + size: usize, + pos: usize, +}; +pub const ZSTD_inBuffer = struct_ZSTD_inBuffer_s; +pub const struct_ZSTD_outBuffer_s = extern struct { + dst: ?*anyopaque, + size: usize, + pos: usize, +}; +pub const ZSTD_outBuffer = struct_ZSTD_outBuffer_s; +pub const ZSTD_CStream = ZSTD_CCtx; +pub extern fn ZSTD_createCStream() ?*ZSTD_CStream; +pub extern fn ZSTD_freeCStream(zcs: ?*ZSTD_CStream) usize; +pub const ZSTD_e_continue: c_int = 0; +pub const ZSTD_e_flush: c_int = 1; +pub const ZSTD_e_end: c_int = 2; +pub const ZSTD_EndDirective = c_uint; +pub extern fn ZSTD_compressStream2(cctx: ?*ZSTD_CCtx, output: [*c]ZSTD_outBuffer, input: [*c]ZSTD_inBuffer, endOp: ZSTD_EndDirective) usize; +pub extern fn ZSTD_CStreamInSize() usize; +pub extern fn ZSTD_CStreamOutSize() usize; +pub extern fn ZSTD_initCStream(zcs: ?*ZSTD_CStream, compressionLevel: c_int) usize; +pub extern fn ZSTD_compressStream(zcs: ?*ZSTD_CStream, output: [*c]ZSTD_outBuffer, input: [*c]ZSTD_inBuffer) usize; +pub extern fn ZSTD_flushStream(zcs: ?*ZSTD_CStream, output: [*c]ZSTD_outBuffer) usize; +pub extern fn ZSTD_endStream(zcs: ?*ZSTD_CStream, output: [*c]ZSTD_outBuffer) usize; +pub const ZSTD_DStream = ZSTD_DCtx; +pub extern fn ZSTD_createDStream() ?*ZSTD_DStream; +pub extern fn ZSTD_freeDStream(zds: ?*ZSTD_DStream) usize; +pub extern fn ZSTD_initDStream(zds: ?*ZSTD_DStream) usize; +pub extern fn ZSTD_decompressStream(zds: ?*ZSTD_DStream, output: [*c]ZSTD_outBuffer, input: [*c]ZSTD_inBuffer) usize; +pub extern fn ZSTD_DStreamInSize() usize; +pub extern fn ZSTD_DStreamOutSize() usize; +pub extern fn ZSTD_compress_usingDict(ctx: ?*ZSTD_CCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, dict: ?*const anyopaque, dictSize: usize, compressionLevel: c_int) usize; +pub extern fn ZSTD_decompress_usingDict(dctx: ?*ZSTD_DCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, dict: ?*const anyopaque, dictSize: usize) usize; +pub const struct_ZSTD_CDict_s = opaque {}; +pub const ZSTD_CDict = struct_ZSTD_CDict_s; +pub extern fn ZSTD_createCDict(dictBuffer: ?*const anyopaque, dictSize: usize, compressionLevel: c_int) ?*ZSTD_CDict; +pub extern fn ZSTD_freeCDict(CDict: ?*ZSTD_CDict) usize; +pub extern fn ZSTD_compress_usingCDict(cctx: ?*ZSTD_CCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, cdict: ?*const ZSTD_CDict) usize; +pub const struct_ZSTD_DDict_s = opaque {}; +pub const ZSTD_DDict = struct_ZSTD_DDict_s; +pub extern fn ZSTD_createDDict(dictBuffer: ?*const anyopaque, dictSize: usize) ?*ZSTD_DDict; +pub extern fn ZSTD_freeDDict(ddict: ?*ZSTD_DDict) usize; +pub extern fn ZSTD_decompress_usingDDict(dctx: ?*ZSTD_DCtx, dst: ?*anyopaque, dstCapacity: usize, src: ?*const anyopaque, srcSize: usize, ddict: ?*const ZSTD_DDict) usize; +pub extern fn ZSTD_getDictID_fromDict(dict: ?*const anyopaque, dictSize: usize) c_uint; +pub extern fn ZSTD_getDictID_fromCDict(cdict: ?*const ZSTD_CDict) c_uint; +pub extern fn ZSTD_getDictID_fromDDict(ddict: ?*const ZSTD_DDict) c_uint; +pub extern fn ZSTD_getDictID_fromFrame(src: ?*const anyopaque, srcSize: usize) c_uint; +pub extern fn ZSTD_CCtx_loadDictionary(cctx: ?*ZSTD_CCtx, dict: ?*const anyopaque, dictSize: usize) usize; +pub extern fn ZSTD_CCtx_refCDict(cctx: ?*ZSTD_CCtx, cdict: ?*const ZSTD_CDict) usize; +pub extern fn ZSTD_CCtx_refPrefix(cctx: ?*ZSTD_CCtx, prefix: ?*const anyopaque, prefixSize: usize) usize; +pub extern fn ZSTD_DCtx_loadDictionary(dctx: ?*ZSTD_DCtx, dict: ?*const anyopaque, dictSize: usize) usize; +pub extern fn ZSTD_DCtx_refDDict(dctx: ?*ZSTD_DCtx, ddict: ?*const ZSTD_DDict) usize; +pub extern fn ZSTD_DCtx_refPrefix(dctx: ?*ZSTD_DCtx, prefix: ?*const anyopaque, prefixSize: usize) usize; +pub extern fn ZSTD_sizeof_CCtx(cctx: ?*const ZSTD_CCtx) usize; +pub extern fn ZSTD_sizeof_DCtx(dctx: ?*const ZSTD_DCtx) usize; +pub extern fn ZSTD_sizeof_CStream(zcs: ?*const ZSTD_CStream) usize; +pub extern fn ZSTD_sizeof_DStream(zds: ?*const ZSTD_DStream) usize; +pub extern fn ZSTD_sizeof_CDict(cdict: ?*const ZSTD_CDict) usize; +pub extern fn ZSTD_sizeof_DDict(ddict: ?*const ZSTD_DDict) usize; +pub const ZSTD_VERSION_MAJOR = @as(c_int, 1); +pub const ZSTD_VERSION_MINOR = @as(c_int, 5); +pub const ZSTD_VERSION_RELEASE = @as(c_int, 5); +pub const ZSTD_VERSION_NUMBER = (((ZSTD_VERSION_MAJOR * @as(c_int, 100)) * @as(c_int, 100)) + (ZSTD_VERSION_MINOR * @as(c_int, 100))) + ZSTD_VERSION_RELEASE; +pub const ZSTD_LIB_VERSION = ZSTD_VERSION_MAJOR.ZSTD_VERSION_MINOR.ZSTD_VERSION_RELEASE; +pub const ZSTD_CLEVEL_DEFAULT = @as(c_int, 3); +pub const ZSTD_MAGICNUMBER = @import("std").zig.c_translation.promoteIntLiteral(c_int, 0xFD2FB528, .hexadecimal); +pub const ZSTD_MAGIC_DICTIONARY = @import("std").zig.c_translation.promoteIntLiteral(c_int, 0xEC30A437, .hexadecimal); +pub const ZSTD_MAGIC_SKIPPABLE_START = @import("std").zig.c_translation.promoteIntLiteral(c_int, 0x184D2A50, .hexadecimal); +pub const ZSTD_MAGIC_SKIPPABLE_MASK = @import("std").zig.c_translation.promoteIntLiteral(c_int, 0xFFFFFFF0, .hexadecimal); +pub const ZSTD_BLOCKSIZELOG_MAX = @as(c_int, 17); +pub const ZSTD_BLOCKSIZE_MAX = @as(c_int, 1) << ZSTD_BLOCKSIZELOG_MAX; +pub const ZSTD_CONTENTSIZE_UNKNOWN = @as(c_ulonglong, 0) - @as(c_int, 1); +pub const ZSTD_CONTENTSIZE_ERROR = @as(c_ulonglong, 0) - @as(c_int, 2); +pub const ZSTD_MAX_INPUT_SIZE = if (@import("std").zig.c_translation.sizeof(usize) == @as(c_int, 8)) @as(c_ulonglong, 0xFF00FF00FF00FF00) else @import("std").zig.c_translation.promoteIntLiteral(c_uint, 0xFF00FF00, .hexadecimal); +pub inline fn ZSTD_COMPRESSBOUND(srcSize: anytype) @TypeOf(if (@import("std").zig.c_translation.cast(usize, srcSize) >= ZSTD_MAX_INPUT_SIZE) @as(c_int, 0) else (srcSize + (srcSize >> @as(c_int, 8))) + (if (srcSize < (@as(c_int, 128) << @as(c_int, 10))) ((@as(c_int, 128) << @as(c_int, 10)) - srcSize) >> @as(c_int, 11) else @as(c_int, 0))) { + return if (@import("std").zig.c_translation.cast(usize, srcSize) >= ZSTD_MAX_INPUT_SIZE) @as(c_int, 0) else (srcSize + (srcSize >> @as(c_int, 8))) + (if (srcSize < (@as(c_int, 128) << @as(c_int, 10))) ((@as(c_int, 128) << @as(c_int, 10)) - srcSize) >> @as(c_int, 11) else @as(c_int, 0)); +} +pub const ZSTD_CCtx_s = struct_ZSTD_CCtx_s; +pub const ZSTD_DCtx_s = struct_ZSTD_DCtx_s; +pub const ZSTD_inBuffer_s = struct_ZSTD_inBuffer_s; +pub const ZSTD_outBuffer_s = struct_ZSTD_outBuffer_s; +pub const ZSTD_CDict_s = struct_ZSTD_CDict_s; +pub const ZSTD_DDict_s = struct_ZSTD_DDict_s; + +// ----------------------------------- + +/// ZSTD_compress() : +/// Compresses `src` content as a single zstd compressed frame into already allocated `dst`. +/// NOTE: Providing `dstCapacity >= ZSTD_compressBound(srcSize)` guarantees that zstd will have +/// enough space to successfully compress the data. +/// @return : compressed size written into `dst` (<= `dstCapacity), +/// or an error code if it fails (which can be tested using ZSTD_isError()). */ +// ZSTDLIB_API size_t ZSTD_compress( void* dst, size_t dstCapacity, +// const void* src, size_t srcSize, +// int compressionLevel); +pub fn compress(dest: []u8, src: []const u8, level: ?i32) Result { + const result = ZSTD_compress(dest.ptr, dest.len, src.ptr, src.len, level orelse ZSTD_defaultCLevel()); + if (ZSTD_isError(result) != 0) return .{ .err = bun.sliceTo(ZSTD_getErrorName(result), 0) }; + return .{ .success = result }; +} + +pub fn compressBound(srcSize: usize) usize { + return ZSTD_compressBound(srcSize); +} + +/// ZSTD_decompress() : +/// `compressedSize` : must be the _exact_ size of some number of compressed and/or skippable frames. +/// `dstCapacity` is an upper bound of originalSize to regenerate. +/// If user cannot imply a maximum upper bound, it's better to use streaming mode to decompress data. +/// @return : the number of bytes decompressed into `dst` (<= `dstCapacity`), +/// or an errorCode if it fails (which can be tested using ZSTD_isError()). */ +// ZSTDLIB_API size_t ZSTD_decompress( void* dst, size_t dstCapacity, +// const void* src, size_t compressedSize); +pub fn decompress(dest: []u8, src: []const u8) Result { + const result = ZSTD_decompress(dest.ptr, dest.len, src.ptr, src.len); + if (ZSTD_isError(result) != 0) return .{ .err = bun.sliceTo(ZSTD_getErrorName(result), 0) }; + return .{ .success = result }; +} + +pub fn getDecompressedSize(src: []const u8) usize { + return ZSTD_getDecompressedSize(src.ptr, src.len); +} + +pub const Result = union(enum) { + success: usize, + err: [:0]const u8, +}; + +const bun = @import("root").bun; diff --git a/src/mdx/mdx_parser.zig b/src/mdx/mdx_parser.zig deleted file mode 100644 index b3dcdb91f..000000000 --- a/src/mdx/mdx_parser.zig +++ /dev/null @@ -1,1835 +0,0 @@ -const std = @import("std"); -const logger = @import("root").bun.logger; -const mdx_lexer = @import("./mdx_lexer.zig"); -const Lexer = mdx_lexer.Lexer; -const importRecord = @import("../import_record.zig"); -const js_ast = bun.JSAst; -const JSParser = @import("../js_parser/js_parser.zig").MDXParser; -const ParseStatementOptions = @import("../js_parser/js_parser.zig").ParseStatementOptions; - -const options = @import("../options.zig"); - -const fs = @import("../fs.zig"); -const bun = @import("root").bun; -const string = bun.string; -const Output = bun.Output; -const Global = bun.Global; -const Environment = bun.Environment; -const strings = bun.strings; -const MutableString = bun.MutableString; -const stringZ = bun.stringZ; -const default_allocator = bun.default_allocator; -const C = bun.C; -const expect = std.testing.expect; -const ImportKind = importRecord.ImportKind; -const BindingNodeIndex = js_ast.BindingNodeIndex; -const Define = @import("../defines.zig").Define; -const js_lexer = bun.js_lexer; -const StmtNodeIndex = js_ast.StmtNodeIndex; -const ExprNodeIndex = js_ast.ExprNodeIndex; -const ExprNodeList = js_ast.ExprNodeList; -const StmtNodeList = js_ast.StmtNodeList; -const BindingNodeList = js_ast.BindingNodeList; -const ParserOptions = @import("../js_parser/js_parser.zig").Parser.Options; -const runVisitPassAndFinish = @import("../js_parser/js_parser.zig").Parser.runVisitPassAndFinish; -const Ref = @import("../ast/base.zig").Ref; -const assert = std.debug.assert; -const BabyList = js_ast.BabyList; - -const LocRef = js_ast.LocRef; -const S = js_ast.S; -const B = js_ast.B; -const G = js_ast.G; -const T = mdx_lexer.T; -const E = js_ast.E; -const Stmt = js_ast.Stmt; -const Expr = js_ast.Expr; -const Binding = js_ast.Binding; -const Symbol = js_ast.Symbol; -const Level = js_ast.Op.Level; -const Op = js_ast.Op; -const Scope = js_ast.Scope; -const Range = logger.Range; - -pub const Container = struct { - ch: u8 = 0, - is_loose: bool = false, - is_task: bool = false, - start: u32 = 0, - mark_indent: u32 = 0, - contents_indent: u32 = 0, - block_index: u32 = 0, - task_mark_off: u32 = 0, -}; - -pub const Block = struct { - tag: Tag = Tag.html, - flags: Block.Flags.Set = Block.Flags.Set{}, - data: u32 = 0, - /// Leaf blocks: Count of lines (MD_LINE or MD_VERBATIMLINE) on the block. - /// LI: Task mark offset in the input doc. - /// OL: Start item number. - /// - line_count: u32 = 0, - line_offset: u32 = 0, - detail: Block.Detail = Block.Detail{ .none = .{} }, - - pub inline fn lines(this: Block, lines_: BabyList(Line)) []Line { - return lines_.ptr[this.line_offset .. this.line_offset + this.line_count]; - } - - pub inline fn verbatimLines(this: Block, lines_: BabyList(Line.Verbatim)) []Line.Verbatim { - return lines_.ptr[this.line_offset .. this.line_offset + this.line_count]; - } - - pub const Data = u32; - - pub const Flags = enum(u3) { - container_opener = 0, - container_closer = 1, - loose_list = 2, - setext_header = 3, - - pub const Set = std.enums.EnumSet(Block.Flags); - }; - - pub inline fn isContainer(this: Block) bool { - return this.flags.contains(.container_opener) or this.flags.contains(.container_closer); - } - - pub const Tag = enum { - /// <body>...</body> - doc, - - /// <blockquote>...</blockquote> - quote, - - /// <ul>...</ul> - ///Detail: Structure ul_detail. - ul, - - /// <ol>...</ol> - ///Detail: Structure ol_detail. - ol, - - /// <li>...</li> - ///Detail: Structure li_detail. - li, - - /// <hr> - hr, - - /// <h1>...</h1> (for levels up to 6) - ///Detail: Structure h_detail. - h, - - /// <pre><code>...</code></pre> - ///Note the text lines within code blocks are terminated with '\n' - ///instead of explicit MD_TEXT_BR. - code, - - /// Raw HTML block. This itself does not correspond to any particular HTML - ///tag. The contents of it _is_ raw HTML source intended to be put - ///in verbatim form to the HTML output. - html, - - /// <p>...</p> - p, - - /// <table>...</table> and its contents. - ///Detail: Structure table_detail (for table), - /// structure td_detail (for th and td) - ///Note all of these are used only if extension MD_FLAG_TABLES is enabled. - table, - thead, - tbody, - tr, - th, - td, - }; - - pub const UL = struct { - tight: bool = false, - mark: u8 = '*', - }; - - pub const OL = struct { - start: u32 = 0, - tight: bool = false, - mark: u8 = '*', - }; - - pub const LI = struct { - /// Can be non-zero only with MD_FLAG_TASKLISTS - task: bool = false, - /// is_task, then one of 'x', 'X' or ' '. Undefined otherwise. - task_mark: u8 = 'x', - /// If is_task, then offset in the input of the char between '[' and ']'. - task_mark_off: u32 = 0, - }; - - pub const Header = u4; - - pub const Code = struct { - info: Attribute = .{}, - lang: Attribute = .{}, - /// character used for fenced code block; or zero for indented code block. * - fence: u8 = '`', - }; - - pub const Table = struct { - /// Count of columns in the table. - column_count: u32 = 0, - /// Count of rows in the table header (currently always 1) - head_row_count: u32 = 1, - /// Count of rows in the table body - body_row_count: u32 = 0, - }; - - pub const Detail = union { - none: void, - ul: UL, - ol: OL, - li: LI, - }; - - pub const TD = struct { - alignment: Align = Align.default, - }; -}; -pub const Span = struct { - pub const Tag = enum { - /// <em>...</em> - em, - - /// <strong>...</strong> - strong, - - /// <a href="xxx">...</a> - /// Detail: Structure a_detail. - a, - - /// <img src="xxx">...</a> - /// Detail: Structure img_detail. - /// Note: Image text can contain nested spans and even nested images. - /// If rendered into ALT attribute of HTML <IMG> tag, it's responsibility - /// of the parser to deal with it. - img, - - /// <code>...</code> - code, - - /// <del>...</del> - /// Note: Recognized only when MD_FLAG_STRIKETHROUGH is enabled. - del, - - /// For recognizing inline ($) and display ($$) equations - /// Note: Recognized only when MD_FLAG_LATEXMATHSPANS is enabled. - latexmath, - latexmath_display, - - /// Wiki links - /// Note: Recognized only when MD_FLAG_WIKILINKS is enabled. - wikilink, - - /// <u>...</u> - /// Note: Recognized only when MD_FLAG_UNDERLINE is enabled. - u, - }; - - pub const Link = struct { - src: Attribute = .{}, - title: Attribute = .{}, - }; - - pub const Image = Link; - - pub const Wikilink = struct { - target: Attribute = .{}, - }; -}; - -pub const Text = enum { - /// Normal text. - normal, - /// NULL character. CommonMark requires replacing NULL character with - /// the replacement char U+FFFD, so this allows caller to do that easily. - nullchar, - /// Line breaks. - /// Note these are not sent from blocks with verbatim output (MD_BLOCK_CODE - /// or MD_BLOCK_HTML). In such cases, '\n' is part of the text itself. - /// <br> (hard break) - br, - /// '\n' in source text where it is not semantically meaningful (soft break) - softbr, - /// Entity. - /// (a) Named entity, e.g. - /// (Note MD4C does not have a list of known entities. - /// Anything matching the regexp /&[A-Za-z][A-Za-z0-9]{1,47};/ is - /// treated as a named entity.) - /// (b) Numerical entity, e.g. Ӓ - /// (c) Hexadecimal entity, e.g. ካ - /// - /// As MD4C is mostly encoding agnostic, application gets the verbatim - /// entity text into the MD_PARSER::text_callback(). - entity, - /// Text in a code block (inside MD_BLOCK_CODE) or inlined code (`code`). - /// If it is inside MD_BLOCK_CODE, it includes spaces for indentation and - /// '\n' for new lines. br and softbr are not sent for this - /// kind of text. - code, - /// Text is a raw HTML. If it is contents of a raw HTML block (i.e. not - /// an inline raw HTML), then br and softbr are not used. - /// The text contains verbatim '\n' for the new lines. - html, - /// Text is inside an equation. This is processed the same way as inlined code - /// spans (`code`). - latexmath, -}; -pub const Align = enum(u3) { - default = 0, - left = 1, - center = 2, - right = 3, -}; - -/// String attribute. -/// -/// This wraps strings which are outside of a normal text flow and which are -/// propagated within various detailed structures, but which still may contain -/// string portions of different types like e.g. entities. -/// -/// So, for example, lets consider this image: -/// -///  -/// -/// The image alt text is propagated as a normal text via the MD_PARSER::text() -/// callback. However, the image title ('foo " bar') is propagated as -/// MD_ATTRIBUTE in MD_SPAN_IMG_DETAIL::title. -/// -/// Then the attribute MD_SPAN_IMG_DETAIL::title shall provide the following: -/// -- [0]: "foo " (substr_types[0] == MD_TEXT_NORMAL; substr_offsets[0] == 0) -/// -- [1]: """ (substr_types[1] == MD_TEXT_ENTITY; substr_offsets[1] == 4) -/// -- [2]: " bar" (substr_types[2] == MD_TEXT_NORMAL; substr_offsets[2] == 10) -/// -- [3]: (n/a) (n/a ; substr_offsets[3] == 14) -/// -/// Note that these invariants are always guaranteed: -/// -- substr_offsets[0] == 0 -/// -- substr_offsets[LAST+1] == size -/// -- Currently, only MD_TEXT_NORMAL, MD_TEXT_ENTITY, MD_TEXT_NULLCHAR -/// substrings can appear. This could change only of the specification -/// changes. -/// -pub const Attribute = struct { - text: []const u8 = "", - substring: Substring.List = .{}, -}; -pub const Substring = struct { - offset: u32, - tag: Text, - - pub const List = std.MultiArrayList(Substring); - pub const ListPool = ObjectPool(List); -}; - -pub const Mark = struct { - position: Ref = Ref.None, - prev: u32 = std.math.maxInt(u32), - next: u32 = std.math.maxInt(u32), - ch: u8 = 0, - flags: u16 = 0, - - /// Maybe closer. - pub const potential_closer = 0x02; - /// Maybe opener. - pub const potential_opener = 0x01; - /// Definitely opener. - pub const opener = 0x04; - /// Definitely closer. - pub const closer = 0x08; - /// Resolved in any definite way. - pub const resolved = 0x10; - - /// Helper for the "rule of 3". */ - pub const emph_intraword = 0x20; - pub const emph_mod3_0 = 0x40; - pub const emph_mod3_1 = 0x80; - pub const emph_mod3_2 = (0x40 | 0x80); - pub const emph_mod3_mask = (0x40 | 0x80); - /// Distinguisher for '<', '>'. */ - pub const autolink = 0x20; - /// For permissive autolinks. */ - pub const validpermissiveautolink = 0x20; - /// For '[' to rule out invalid link labels early */ - pub const hasnestedbrackets = 0x20; - - /// During analyzes of inline marks, we need to manage some "mark chains", - /// of (yet unresolved) openers. This structure holds start/end of the chain. - /// The chain internals are then realized through MD_MARK::prev and ::next. - pub const Chain = struct { - head: u32 = std.math.maxInt(u32), - tail: u32 = std.math.maxInt(u32), - - pub const List = struct { - data: [13]Chain = [13]Chain{ .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{}, .{} }, - pub inline fn ptr_chain(this: *List) *Chain { - return &this.data[0]; - } - pub inline fn tablecellboundaries(this: *List) *Chain { - return &this.data[1]; - } - pub inline fn asterisk_openers_extraword_mod3_0(this: *List) *Chain { - return &this.data[2]; - } - pub inline fn asterisk_openers_extraword_mod3_1(this: *List) *Chain { - return &this.data[3]; - } - pub inline fn asterisk_openers_extraword_mod3_2(this: *List) *Chain { - return &this.data[4]; - } - pub inline fn asterisk_openers_intraword_mod3_0(this: *List) *Chain { - return &this.data[5]; - } - pub inline fn asterisk_openers_intraword_mod3_1(this: *List) *Chain { - return &this.data[6]; - } - pub inline fn asterisk_openers_intraword_mod3_2(this: *List) *Chain { - return &this.data[7]; - } - pub inline fn underscore_openers(this: *List) *Chain { - return &this.data[8]; - } - pub inline fn tilde_openers_1(this: *List) *Chain { - return &this.data[9]; - } - pub inline fn tilde_openers_2(this: *List) *Chain { - return &this.data[10]; - } - pub inline fn bracket_openers(this: *List) *Chain { - return &this.data[11]; - } - pub inline fn dollar_openers(this: *List) *Chain { - return &this.data[12]; - } - }; - }; -}; - -pub const Line = struct { - beg: u32 = 0, - end: u32 = 0, - - pub const Tag = enum(u32) { - blank, - hr, - atx_header, - setext_header, - setext_underline, - indented_code, - fenced_code, - html, - text, - table, - table_underline, - }; - pub const Analysis = packed struct { - tag: Tag = Tag.blank, - beg: u32 = 0, - end: u32 = 0, - indent: u32 = 0, - data: u32 = 0, - - pub const blank = Analysis{}; - pub fn eql(a: Analysis, b: Analysis) bool { - return strings.eqlLong(std.mem.asBytes(&a), std.mem.asBytes(&b), false); - } - }; - - pub const Verbatim = struct { - line: Line = Line{}, - indent: u32 = 0, - }; -}; - -pub const MDParser = struct { - marks: BabyList(Mark) = .{}, - chain: Mark.Chain.List = .{}, - source: logger.Source, - flags: Flags.Set = Flags.commonmark, - allocator: std.mem.Allocator, - mdx: *MDX, - mark_char_map: [255]u1 = undefined, - doc_ends_with_newline: bool = false, - size: u32 = 0, - - lines: BabyList(Line) = .{}, - verbatim_lines: BabyList(Line.Verbatim) = .{}, - - containers: BabyList(Container) = .{}, - blocks: BabyList(Block) = .{}, - current_block: ?*Block = null, - current_block_index: u32 = 0, - - code_fence_length: u32 = 0, - code_indent_offset: u32 = std.math.maxInt(u32), - last_line_has_list_loosening_effect: bool = false, - last_list_item_starts_with_two_blank_lines: bool = false, - - pub const Flags = enum { - /// In MD_TEXT_NORMAL, collapse non-trivial whitespace into single ' ' - collapse_whitespace, - /// Do not require space in ATX headers ( ###header ) - permissive_atxheaders, - /// Recognize URLs as autolinks even without '<', '>' - permissive_url_autolinks, - /// Recognize e-mails as autolinks even without '<', '>' and 'mailto:' - permissive_email_autolinks, - /// Disable indented code blocks. (Only fenced code works.) - noindented_codeblocks, - /// Disable raw HTML blocks. - no_html_blocks, - /// Disable raw HTML (inline). - no_html_spans, - /// Enable tables extension. - tables, - /// Enable strikethrough extension. - strikethrough, - /// Enable WWW autolinks (even without any scheme prefix, if they begin with 'www.') - permissive_www_autolinks, - /// Enable task list extension. - tasklists, - /// Enable $ and $$ containing LaTeX equations. - latex_mathspans, - /// Enable wiki links extension. - wikilinks, - /// Enable underline extension (and disables '_' for normal emphasis). - underline, - - pub const Set = std.enums.EnumSet(Flags); - pub const permissive_autolinks = Set.init(.{ .permissive_email_autolinks = true, .permissive_url_autolinks = true }); - pub const no_email = Set.init(.{ .no_html_blocks = true, .no_html_spans = true }); - pub const github = Set.init(.{ .tables = true, .permissive_autolinks = true, .strikethrough = true, .tasklists = true }); - pub const commonmark: i32 = Set{}; - }; - - fn buildCharMap(this: *MDParser) void { - @memset(&this.mark_char_map, 0, this.mark_char_map.len); - - this.mark_char_map['\\'] = 1; - this.mark_char_map['*'] = 1; - this.mark_char_map['_'] = 1; - this.mark_char_map['`'] = 1; - this.mark_char_map['&'] = 1; - this.mark_char_map[';'] = 1; - this.mark_char_map['<'] = 1; - this.mark_char_map['>'] = 1; - this.mark_char_map['['] = 1; - this.mark_char_map['!'] = 1; - this.mark_char_map[']'] = 1; - this.mark_char_map[0] = 1; - - // whitespace - this.mark_char_map[' '] = 1; - this.mark_char_map['\t'] = 1; - this.mark_char_map['\r'] = 1; - this.mark_char_map['\n'] = 1; - - // form feed - this.mark_char_map[0xC] = 1; - // vertical tab - this.mark_char_map[0xB] = 1; - - if (this.flags.contains(.strikethrough)) { - this.mark_char_map['~'] = 1; - } - - if (this.flags.contains(.latex_mathspans)) { - this.mark_char_map['$'] = 1; - } - - if (this.flags.contains(.permissive_email_autolinks)) { - this.mark_char_map['@'] = 1; - } - - if (this.flags.contains(.permissive_url_autolinks)) { - this.mark_char_map[':'] = 1; - } - - if (this.flags.contains(.permissive_www_autolinks)) { - this.mark_char_map['.'] = 1; - } - - if (this.flags.contains(.tables)) { - this.mark_char_map['.'] = 1; - } - } - pub fn init(allocator: std.mem.Allocator, source: logger.Source, flags: Flags.Set, mdx: *MDX) MDParser { - var parser = MDParser{ - .allocator = allocator, - .source = source, - .flags = flags, - .mdx = mdx, - .size = @truncate(u32, source.contents.len), - }; - parser.buildCharMap(); - parser.doc_ends_with_newline = source.contents.len.len > 0 and source.contents[source.contents.len - 1] == '\n'; - return parser; - } - - fn startNewBlock(this: *MDParser, line: *const Line.Analysis) !void { - try this.blocks.push( - this.allocator, - Block{ - .tag = switch (line.tag) { - .hr => Block.Tag.hr, - .atx_header, .setext_header => Block.Tag.h, - .fenced_code, .indented_code => Block.Tag.code, - .text => Block.Tag.p, - .html => Block.Tag.html, - else => unreachable, - }, - .data = line.data, - .line_count = 0, - .line_offset = switch (line.tag) { - .indented_code, .html, .fenced_code => this.verbatim_lines.len, - else => this.lines.len, - }, - }, - ); - } - - inline fn charAt(this: *const MDParser, index: u32) u8 { - return this.source.contents[index]; - } - - inline fn isNewline(this: *const MDParser, index: u32) bool { - return switch (this.charAt(index)) { - '\n', '\r' => true, - else => false, - }; - } - - inline fn isAnyOf2(this: *const MDParser, index: u32, comptime first: u8, comptime second: u8) bool { - return isAnyOf2_(this.charAt(index), first, second); - } - - inline fn isAnyOf2_(char: u8, comptime first: u8, comptime second: u8) bool { - return switch (char) { - first, second => true, - else => false, - }; - } - - inline fn isAnyOf(this: *const MDParser, index: u32, comptime values: []const u8) bool { - return isCharAnyOf(this.charAt(index), values); - } - - inline fn isCharAnyOf(char: u8, comptime values: []const u8) bool { - inline for (values) |val| { - if (val == char) return true; - } - return false; - } - - inline fn isBlank(char: u8) bool { - return isCharAnyOf(char, &[_]u8{ ' ', '\t' }); - } - - inline fn isWhitespace(char: u8) bool { - return isCharAnyOf(char, &[_]u8{ ' ', '\t', 0xC, 0xB }); - } - - pub fn getIndent(this: *MDParser, total_indent: u32, beg: u32, end: *u32) u32 { - var off = beg; - var indent = total_indent; - while (off < this.size and isBlank(this.charAt(off))) { - if (this.charAt(off) == '\t') { - indent = (indent + 4) & ~3; - } else { - indent += 1; - } - off += 1; - } - end.* = off; - return indent - total_indent; - } - - pub fn isContainerMark(this: *MDParser, indent: u32, beg: u32, end: *u32, container: *Container) bool { - var off = beg; - var max_end: u32 = undefined; - - if (off >= this.size or indent >= this.code_indent_offset) - return false; - - if (this.charAt(off) == '>') { - off += 1; - container.ch = '>'; - container.is_loose = false; - container.is_task = false; - container.mark_indent = indent; - container.contents_indent = indent + 1; - end.* = off; - return true; - } - - // Check for list item bullet mark. - if (this.isAnyOf(off, "-+*") and (off + 1 >= this.size or isBlank(this.charAt(off + 1)) or this.isNewline(off + 1))) { - container.ch = this.charAt(off); - container.is_loose = false; - container.is_task = false; - container.mark_indent = indent; - container.contents_indent = indent + 1; - end.* = off + 1; - return true; - } - - // Check for ordered list item marks - max_end = @min(off + 9, this.size); - container.start = 0; - while (off < max_end and std.ascii.isDigit(this.charAt(off))) { - container.start = container.start * 10 + (this.charAt(off) - '0'); - off += 1; - } - - if (off > beg and - off < this.size and - (this.isAnyOf2(off, '.', ')')) and - (off + 1 >= this.size or - this.isBlank(this.charAt(off + 1) or - this.isNewline(off + 1)))) - { - container.ch = this.charAt(off); - container.is_loose = false; - container.is_task = false; - container.mark_indent = indent; - container.contents_indent = indent + off - beg + 1; - end.* = off + 1; - return true; - } - - return false; - } - - fn analyzeLine(this: *MDParser, beg: u32, end: *u32, pivot_line: *const Line.Analysis, line: *Line.Analysis) !void { - _ = this; - _ = beg; - _ = end; - _ = pivot_line; - _ = line; - var off = beg; - var hr_killer: u32 = 0; - var prev_line_has_list_loosening_effect = this.last_line_has_list_loosening_effect; - var container = Container{}; - _ = hr_killer; - _ = prev_line_has_list_loosening_effect; - _ = container; - var total_indent: u32 = 0; - var n_parents: u32 = 0; - var n_brothers: u32 = 0; - var n_children: u32 = 0; - - // Given the indentation and block quote marks '>', determine how many of - // the current containers are our parents. - while (n_parents < this.containers.len) { - var c: *Container = this.containers.ptr + n_parents; - - if (c.ch == '>' and line.indent < this.code_indent_offset and off < this.size and this.charAt(off) == '>') { - off += 1; - total_indent += 1; - line.indent = this.getIndent(total_indent, off, &off); - total_indent += line.indent; - - // The optional 1st space after '>' is part of the block quote mark. - line.indent -|= line.indent; - line.beg = off; - } else if (c.ch != '>' and line.indent >= c.contents_indent) { - line.indent -|= c.contents_indent; - } else { - break; - } - - n_parents += 1; - } - - if (off >= this.size or this.isNewline(off)) { - // Blank line does not need any real indentation to be nested inside a list - if (n_brothers + n_children == 0) { - while (n_parents < this.containers.len and this.containers.ptr[n_parents].ch == '>') { - n_parents += 1; - } - } - } - - while (true) { - switch (pivot_line.tag) { - .fencedcode => { - // Check whether we are fenced code continuation. - line.beg = off; - - // We are another MD_LINE_FENCEDCODE unless we are closing fence - // which we transform into MD_LINE_BLANK. - if (line.indent < this.code_indent_offset) { - if (this.isClosingCodeFence(this.charAt(pivot_line.beg), off, &off)) { - line.tag = .blank; - this.last_line_has_list_loosening_effect = false; - break; - } - } - - // Change indentation accordingly to the initial code fence. - if (n_parents == this.containers.len) { - line.indent -|= pivot_line.indent; - line.tag = .fenced_code; - break; - } - }, - - .indentedcode => {}, - .text => {}, - - .html => {}, - else => {}, - } - - // Check for blank line. - if (off >= this.size or this.isNewline(off)) { - if (pivot_line.tag == .indented_code and n_parents == this.containers.len) { - line.tag = .indented_code; - line.indent -|= this.code_indent_offset; - this.last_line_has_list_loosening_effect = false; - } else { - line.tag = .blank; - this.last_line_has_list_loosening_effect = n_parents > 0 and - n_brothers + n_children == 0 and - this.containers.ptr[n_parents - 1].ch != '>'; - - // See https://github.com/mity/md4c/issues/6 - // - // This ugly checking tests we are in (yet empty) list item but - // not its very first line (i.e. not the line with the list - // item mark). - // - // If we are such a blank line, then any following non-blank - // line which would be part of the list item actually has to - // end the list because according to the specification, "a list - // item can begin with at most one blank line." - // - if (n_parents > 0 and this.containers.ptr[n_parents - 1].ch != '>' and n_brothers + n_children == 0 and this.current_block == null and this.blocks.len > 0) { - var top_block = this.blocks.last().?; - if (top_block.tag == .li) { - this.last_list_item_starts_with_two_blank_lines = true; - } - } - } - break; - } else { - // This is the 2nd half of the hack. If the flag is set (i.e. there - // was a 2nd blank line at the beginning of the list item) and if - // we would otherwise still belong to the list item, we enforce - // the end of the list. - this.last_line_has_list_loosening_effect = false; - if (this.last_list_item_starts_with_two_blank_lines) { - if (n_parents > 0 and - this.containers.ptr[n_parents - 1].ch != '>' and - n_brothers + n_children == 0 and - this.current_block == null and this.blocks.len > 1) - { - var top = this.blocks.last().?; - if (top.tag == .li) { - n_parents -|= 1; - } - } - this.last_line_has_list_loosening_effect = true; - } - } - - // Check whether we are Setext underline. - if (line.indent < this.code_indent_offset and - pivot_line.tag == .text and - off < this.size and - this.isAnyOf2(off, '=', '-') and - n_parents == this.containers.len) - { - var level: u4 = 0; - if (this.isSetextUnderline(off, &off, &level)) { - line.tag = .setext_underline; - line.data = level; - break; - } - } - - // Check for a thematic break line - if (line.indent < this.code_indent_offset and off < this.size and off >= hr_killer and this.isAnyOf(off, "-_*")) { - if (this.isHRLine(off, &off, &hr_killer)) { - line.tag = .hr; - break; - } - } - - // Check for "brother" container. I.e. whether we are another list item - //in already started list. - if (n_parents < this.containers.len and n_brothers + n_children == 0) { - var tmp: u32 = undefined; - - if (this.isContainerMark(line.indent, off, &tmp, &container) and - isContainerCompatible(&this.containers.ptr[n_parents], &container)) - { - pivot_line.* = Line.Analysis.blank; - off = tmp; - - total_indent += container.contents_indent - container.mark_indent; - line.indent = this.getIndent(total_indent, off, &off); - total_indent += line.indent; - line.beg = off; - - // Some of the following whitespace actually still belongs to the mark. - if (off >= this.size or this.isNewline(off)) { - container.contents_indent += 1; - } else if (line.indent <= this.code_indent_offset) { - container.contents_indent += line.indent; - line.indent = 0; - } else { - container.contents_indent += 1; - line.indent -= 1; - } - - this.containers.ptr[n_parents].mark_indent = container.mark_indent; - this.containers.ptr[n_parents].contents_indent = container.contents_indent; - n_brothers += 1; - continue; - } - } - - // Check for indented code - // Note: indented code block cannot interrupt a paragrpah - if (line.indent >= this.code_indent_offset and - (pivot_line.tag == .blank or - pivot_line.tag == .indented_code)) - { - line.tag = .indented_code; - std.debug.assert(line.indent >= this.code_indent_offset); - line.indent -|= this.code_indent_offset; - line.data = 0; - break; - } - - // Check for start of a new container block - if (line.indent < this.code_indent_offset and - this.isContainerMark(line.indent, off, &off, &container)) - { - if (pivot_line.tag == .text and - n_parents == this.n_containers and - (off >= this.size or this.isNewline(off)) and - container.ch != '>') - { - // Noop. List mark followed by a blank line cannot interrupt a paragraph. - } else if (pivot_line.tag == .text and - n_parents == this.containers.len and - isAnyOf2_(container.ch, '.', ')')) - { - // Noop. Ordered list cannot interrupt a paragraph unless the start index is 1. - } else { - total_indent += container.contents_indent - container.mark_indent; - line.indent = this.getIndent(total_indent, off, &off); - total_indent += line.indent; - - line.beg = off; - line.data = container.ch; - - // Some of the following whitespace actually still belongs to the mark. - if (off >= this.size or this.isNewline(off)) { - container.contents_indent += 1; - } else if (line.indent <= this.code_indent_offset) { - container.contents_indent += line.indent; - line.indent = 0; - } else { - container.contents_indent += 1; - line.indent -= 1; - } - - if (n_brothers + n_children == 0) { - pivot_line.* = Line.Analysis.blank; - } - - if (n_children == 0) { - try this.leaveChildContainers(n_parents + n_brothers); - } - - n_children += 1; - try this.pushContainer(container); - continue; - } - } - - // heck whether we are table continuation. - if (pivot_line.tag == .table and n_parents == this.n_containers) { - line.tag = .table; - break; - } - - // heck for ATX header. - if (line.indent < this.code_indent_offset and off < this.size and this.isAnyOf(off, '#')) { - var level: u4 = 0; - if (this.isATXHeaderLine(off, &line.beg, &off, &level)) { - line.tag = .atx_header; - line.data = level; - break; - } - } - - // Check whether we are starting code fence. - if (off < this.size and this.isAnyOf2(off, '`', '~')) { - if (this.isOpeningCodeFence(off, &off)) { - line.tag = .fenced_code; - line.data = 1; - break; - } - } - - // Check for start of raw HTML block. - if (off < this.size and !this.flags.contains(.no_html_blocks) and this.charAt(off) == '<') {} - - // Check for table underline. - if (this.flags.contains(.tables) and pivot_line.tag == .text and off < this.size and this.isAnyOf(off, "|-:") and n_parents == this.containers.len) { - var col_count: u32 = undefined; - - if (this.current_block != null and this.current_block.?.line_count == 1 and this.isTableUnderline(off, &off, &col_count)) { - line.data = col_count; - line.tag = .table_underline; - break; - } - } - - // By default, we are normal text line. - line.tag = .text; - if (pivot_line.tag == .text and n_brothers + n_children == 0) { - // lazy continuation - n_parents = this.containers.len; - } - - // Check for task mark. - if (this.flags.contains(.tasklists) and - n_brothers + n_children > 0 and - off < this.size and - isCharAnyOf(this.containers.last().?.ch, "-+*.)")) - { - var tmp: u32 = off; - - while (tmp < this.size and tmp < off + 3 and isBlank(tmp)) { - tmp += 1; - } - - if ((tmp + 2 < this.size and - this.charAt(tmp) == '[' and - this.isAnyOf(tmp + 1, "xX ") and - this.charAt(tmp + 2) == ']') and - (tmp + 3 == this.size or - isBlank(this.charAt(tmp + 3)) or - this.isNewline(tmp + 3))) - { - var task_container: *Container = if (n_children > 0) this.containers.last().? else &container; - task_container.is_task = true; - task_container.task_mark_off = tmp + 1; - off = tmp + 3; - while (off < this.size and isWhitespace(this.charAt(off))) { - off += 1; - } - if (off == this.size) break; - line.beg = off; - } - } - - break; - } - - // Scan for end of the line. - while (!(strings.hasPrefixComptime(this.source.contents.ptr[off..], "\n\n\n\n") or - strings.hasPrefixComptime(this.source.contents.ptr[off..], "\r\n\r\n"))) - { - off += 4; - } - - while (off < this.size and !this.isNewline(off)) { - off += 1; - } - - // Set end of line - line.end = off; - - // ut for ATX header, we should exclude the optional trailing mark. - if (line.type == .atx_header) { - var tmp = line.end; - while (tmp > line.beg and this.charAt(tmp - 1) == ' ') { - tmp -= 1; - } - - while (tmp > line.beg and this.charAt(tmp - 1) == '#') { - tmp -= 1; - } - - if (tmp == line.beg or this.charAt(tmp - 1) == ' ' or this.flags.contains(.permissive_atxheaders)) { - line.end = tmp; - } - } - - // Trim trailing spaces. - switch (line.tag) { - .indented_code, .fenced_code => {}, - else => { - while (line.end > line.beg and this.charAt(line.end - 1) == ' ') { - line.end -= 1; - } - }, - } - - // Eat also the new line - if (off < this.size and this.charAt(off) == '\r') { - off += 1; - } - - if (off < this.size and this.charAt(off) == '\n') { - off += 1; - } - - end.* = off; - - // If we belong to a list after seeing a blank line, the list is loose. - if (prev_line_has_list_loosening_effect and line.tag != .blank and n_parents + n_brothers > 0) { - var c: *Container = this.containers.ptr[n_parents + n_brothers - 1]; - if (c.ch != '>') { - var block: *Block = this.blocks.ptr[c.block_index]; - block.flags.insert(.loose_list); - } - } - - // Leave any containers we are not part of anymore. - if (n_children == 0 and n_parents + n_brothers < this.containers.len) { - try this.leaveChildContainers(n_parents + n_brothers); - } - - // Enter any container we found a mark for - if (n_brothers > 0) { - std.debug.assert(n_brothers == 0); - try this.pushContainerBytes( - Block.Tag.li, - this.containers.ptr[n_parents].task_mark_off, - if (this.containers.ptr[n_parents].is_task) this.charAt(this.containers.ptr[n_parents].task_mark_off) else 0, - Block.Flags.container_closer, - ); - try this.pushContainerBytes( - Block.Tag.li, - container.task_mark_off, - if (container.is_task) this.charAt(container.task_mark_off) else 0, - Block.Flags.container_opener, - ); - this.containers.ptr[n_parents].is_task = container.is_task; - this.containers.ptr[n_parents].task_mark_off = container.task_mark_off; - } - - if (n_children > 0) { - try this.enterChildContainers(n_children); - } - } - fn processLine(this: *MDParser, p_pivot_line: **const Line.Analysis, line: *Line.Analysis) !void { - var pivot_line = p_pivot_line.*; - - switch (line.tag) { - .blank => { - // Blank line ends current leaf block. - try this.endCurrentBlock(); - p_pivot_line.* = Line.Analysis.blank; - }, - .hr, .atx_header => { - try this.endCurrentBlock(); - - // Add our single-line block - try this.startNewBlock(line); - try this.addLineIntoCurrentBlock(line); - try this.endCurrentBlock(); - p_pivot_line.* = &Line.Analysis.blank; - }, - .setext_underline => { - this.current_block.?.tag = .table; - this.current_block.?.data = line.data; - this.current_block.?.flags.insert(.setext_header); - try this.addLineIntoCurrentBlock(line); - try this.endCurrentBlock(); - if (this.current_block == null) { - p_pivot_line.* = &Line.Analysis.blank; - } else { - // This happens if we have consumed all the body as link ref. defs. - //and downgraded the underline into start of a new paragraph block. - line.tag = .text; - p_pivot_line.* = line; - } - }, - // MD_LINE_TABLEUNDERLINE changes meaning of the current block. - .table_underline => { - var current_block = this.current_block.?; - std.debug.assert(current_block.line_count == 1); - current_block.tag = .table; - current_block.data = line.data; - std.debug.assert(pivot_line != &Line.Analysis.blank); - @intToPtr(*Line.Analysis, @ptrToInt(p_pivot_line.*)).tag = .table; - try this.addLineIntoCurrentBlock(line); - }, - else => { - // The current block also ends if the line has different type. - if (line.tag != pivot_line.tag) { - try this.endCurrentBlock(); - } - - // The current line may start a new block. - if (this.current_block == null) { - try this.startNewBlock(line); - p_pivot_line.* = line; - } - - // In all other cases the line is just a continuation of the current block. - try this.addLineIntoCurrentBlock(line); - }, - } - } - fn consumeLinkReferenceDefinitions(this: *MDParser) !void { - _ = this; - } - fn addLineIntoCurrentBlock(this: *MDParser, analysis: *const Line.Analysis) !void { - var current_block = this.current_block.?; - - switch (current_block.tag) { - .code, .html => { - if (current_block.line_count > 0) - std.debug.assert( - this.verbatim_lines.len == current_block.line_count + current_block.line_offset, - ); - if (current_block.line_count == 0) { - current_block.line_offset = this.verbatim_lines.len; - } - - try this.verbatim_lines.push(this.allocator, Line.Verbatim{ - .indent = analysis.indent, - .line = .{ - .beg = analysis.beg, - .end = analysis.end, - }, - }); - }, - else => { - if (current_block.line_count > 0) - std.debug.assert( - this.lines.len == current_block.line_count + current_block.line_offset, - ); - if (current_block.line_count == 0) { - current_block.line_offset = this.lines.len; - } - this.lines.push(this.allocator, .{ .beg = analysis.beg, .end = analysis.end }); - }, - } - - current_block.line_count += 1; - } - fn endCurrentBlock(this: *MDParser) !void { - _ = this; - - var block = this.current_block orelse return; - // Check whether there is a reference definition. (We do this here instead - // of in md_analyze_line() because reference definition can take multiple - // lines.) */ - if ((block.tag == .p or block.tag == .h) and block.flags.contains(.setext_header)) { - var lines = block.lines(this.lines); - if (lines[0].beg == '[') { - try this.consumeLinkReferenceDefinitions(); - block = this.current_block orelse return; - } - } - - if (block.tag == .h and block.flags.contains(.setext_header)) { - var n_lines = block.line_count; - if (n_lines > 1) { - // get rid of the underline - if (this.lines.len == block.line_count + block.line_offset) { - this.lines.len -= 1; - } - block.line_count -= 1; - } else { - // Only the underline has left after eating the ref. defs. - // Keep the line as beginning of a new ordinary paragraph. */ - block.tag = .p; - } - } - - // Mark we are not building any block anymore. - this.current_block = null; - this.current_block_index -|= 1; - } - fn buildRefDefHashTable(this: *MDParser) !void { - _ = this; - } - fn leaveChildContainers(this: *MDParser, keep: u32) !void { - _ = this; - while (this.containers.len > keep) { - var c = this.containers.last().?; - var is_ordered_list = false; - switch (c.ch) { - ')', '.' => { - is_ordered_list = true; - }, - '-', '+', '*' => { - try this.pushContainerBytes( - Block.Tag.li, - c.task_mark_off, - if (c.is_task) this.charAt(c.task_mark_off) else 0, - Block.Flags.container_closer, - ); - try this.pushContainerBytes( - if (is_ordered_list) Block.Tag.ol else Block.Tag.ul, - c.ch, - if (c.is_task) this.charAt(c.task_mark_off) else 0, - Block.Flags.container_closer, - ); - }, - '>' => { - try this.pushContainerBytes( - Block.Tag.quote, - 0, - 0, - Block.Flags.container_closer, - ); - }, - else => unreachable, - } - - this.containers.len -= 1; - } - } - fn enterChildContainers(this: *MDParser, keep: u32) !void { - _ = this; - var i: u32 = this.containers.len - keep; - while (i < this.containers.len) : (i += 1) { - var c: *Container = this.containers.ptr[i]; - var is_ordered_list = false; - - switch (c.ch) { - ')', '.' => { - is_ordered_list = true; - }, - '-', '+', '*' => { - // Remember offset in ctx.block_bytes so we can revisit the - // block if we detect it is a loose list. - try this.endCurrentBlock(); - c.block_index = this.blocks.len; - - try this.pushContainerBytes( - if (is_ordered_list) Block.Tag.ol else Block.Tag.ul, - c.start, - c.ch, - Block.Flags.container_opener, - ); - try this.pushContainerBytes( - Block.Tag.li, - c.task_mark_off, - if (c.is_task) this.charAt(c.task_mark_off) else 0, - Block.Flags.container_opener, - ); - }, - '>' => { - try this.pushContainerBytes( - Block.Tag.quote, - 0, - 0, - Block.Flags.container_opener, - ); - }, - else => unreachable, - } - } - } - fn pushContainer(this: *MDParser, container: Container) !void { - try this.containers.push(this.allocator, container); - } - - fn processLeafBlock(this: *MDParser, comptime tag: Block.Tag, block: *Block) anyerror!void { - const BlockDetailType = comptime switch (tag) { - Block.Tag.h => Block.Header, - Block.Tag.code => Block.Code, - Block.Tag.table => Block.Table, - }; - - const is_in_tight_list = if (this.containers.len == 0) - false - else - !this.containers.ptr[this.containers.len - 1].is_loose; - - const detail: BlockDetailType = switch (comptime tag) { - Block.Tag.h => @truncate(Block.Header, block.data), - Block.Tag.code => try this.setupFencedCodeDetail(block), - Block.Tag.table => .{ - .col_count = block.data, - .head_row_count = 1, - .body_row_count = block.line_count -| 2, - }, - else => {}, - }; - - if (!is_in_tight_list or comptime tag != .p) { - try this.mdx.onEnterBlock(block.tag, BlockDetailType, detail); - } - - defer { - if (comptime tag == Block.Tag.code) {} - } - } - - fn pushContainerBytes(this: *MDParser, block_type: Block.Tag, start: u32, data: u32, flag: Block.Flags) !void { - try this.endCurrentBlock(); - var block = Block{ - .tag = block_type, - .line_count = start, - .data = data, - }; - block.flags.insert(flag); - var prev_block: ?Block = null; - if (this.current_block) |curr| { - prev_block = curr.*; - } - - try this.blocks.push(this.allocator, block); - if (prev_block != null) { - this.current_block = this.blocks.ptr[this.current_block_index]; - } - } - fn processBlock(this: *MDParser, comptime tag: Block.Tag, block: *Block) !void { - const detail: Block.Detail = - switch (comptime tag) { - .ul => Block.Detail{ - .ul = .{ - .is_tight = !block.flags.contains(.loose_list), - .mark = @truncate(u8, block.data), - }, - }, - .ol => Block.Detail{ - .ol = .{ - .start = block.line_count, - .is_tight = !block.flags.contains(.loose_list), - .mark_delimiter = @truncate(u8, block.data), - }, - }, - .li => Block.Detail{ - .li = .{ - .is_task = block.data != 0, - .task_mark = @truncate(u8, block.data), - .task_mark_offset = @intCast(u32, block.line_count), - }, - }, - else => Block.Detail{ .none = .{} }, - }; - - if (block.flags.contains(.container)) { - if (block.flags.contains(.container_closer)) { - switch (block.tag) { - .li => try this.mdx.onLeaveBlock(tag, Block.LI, detail.li), - .ul => try this.mdx.onLeaveBlock(tag, Block.UL, detail.ul), - .ol => try this.mdx.onLeaveBlock(tag, Block.OL, detail.ol), - else => try this.mdx.onLeaveBlock(block.tag, void, {}), - } - this.containers.len -|= switch (block.tag) { - .ul, .ol, .blockquote => 1, - else => 0, - }; - } - - if (block.flags.contains(.container_opener)) { - switch (comptime tag) { - .li => try this.mdx.onEnterBlock(tag, Block.LI, detail.li), - .ul => try this.mdx.onEnterBlock(tag, Block.UL, detail.ul), - .ol => try this.mdx.onEnterBlock(tag, Block.OL, detail.ol), - else => try this.mdx.onEnterBlock(block.tag, void, {}), - } - - switch (comptime tag) { - .ul, .ol => { - this.containers.ptr[this.containers.len].is_loose = block.flags.contains(.loose_list); - this.containers.len += 1; - }, - .blockquote => { - // This causes that any text in a block quote, even if - // nested inside a tight list item, is wrapped with - // <p>...</p>. */ - this.containers.ptr[this.containers.len].is_loose = true; - this.containers.len += 1; - }, - else => {}, - } - } - } else { - try this.processLeafBlock(tag, block); - } - } - fn processAllBlocks(this: *MDParser) !void { - _ = this; - - // ctx->containers now is not needed for detection of lists and list items - // so we reuse it for tracking what lists are loose or tight. We rely - // on the fact the vector is large enough to hold the deepest nesting - // level of lists. - this.containers.len = 0; - var blocks = this.blocks.slice(); - for (&blocks) |*block| {} - } - fn isContainerCompatible(pivot: *const Container, container: *const Container) bool { - // Block quote has no "items" like lists. - if (container.ch == '>') return false; - - if (container.ch != pivot.ch) - return false; - - if (container.mark_indent > pivot.contents_indent) - return false; - return true; - } - - fn isHRLine(this: *MDParser, beg: u32, end: *u32, hr_killer: *u32) bool { - var off = beg + 1; - var n: u32 = 1; - - while (off < this.size and (this.charAt(off) == this.charAt(beg) or this.charAt(off) == ' ' or this.charAt(off) == '\t')) { - if (this.charAt(off) == this.charAt(beg)) - n += 1; - off += 1; - } - - if (n < 3) { - hr_killer.* = off; - return false; - } - - // Nothing else can be present on the line. */ - if (off < this.size and !this.isNewline(off)) { - hr_killer.* = off; - return false; - } - - end.* = off; - return true; - } - - fn isSetextUnderline(this: *MDParser, beg: u32, end: *u32, level: *u4) bool { - var off = beg + 1; - while (off < this.size and this.charAt(off) == this.charAt(beg)) - off += 1; - - // Optionally, space(s) can follow. */ - while (off < this.size and this.charAt(off) == ' ') - off += 1; - - // But nothing more is allowed on the line. - if (off < this.size and !this.isNewline(off)) - return false; - level.* = if (this.charAt(beg) == '=') 1 else 2; - end.* = off; - return true; - } - - fn isATXHeaderLine(this: *MDParser, beg: u32, p_beg: *u32, end: *u32, level: *u4) bool { - var n: i32 = undefined; - var off: u32 = beg + 1; - - while (off < this.size and this.charAt(off) == '#' and off - beg < 7) { - off += 1; - } - n = off - beg; - - if (n > 6) - return false; - level.* = @intCast(u4, n); - - if (!(this.flags.contains(.permissive_atxheaders)) and off < this.size and - this.charAt(off) != ' ' and this.charAt(off) != '\t' and !this.isNewline(off)) - return false; - - while (off < this.size and this.charAt(off) == ' ') { - off += 1; - } - - p_beg.* = off; - end.* = off; - - return true; - } - - fn isTableUnderline(this: *MDParser, beg: u32, end: *u32, column_column: *u32) bool { - _ = this; - _ = end; - _ = column_column; - - var off = beg; - var found_pipe = false; - var col_count: u32 = 0; - - if (off < this.size and this.charAt(off) == '|') { - found_pipe = true; - off += 1; - while (off < this.size and isWhitespace(this.charAt(off))) { - off += 1; - } - } - - while (true) { - var delimited = false; - - // Cell underline ("-----", ":----", "----:" or ":----:")if(off < this.size and this.charAt(off) == _T(':')) - off += 1; - if (off >= this.size or this.charAt(off) != '-') - return false; - while (off < this.size and this.charAt(off) == '-') - off += 1; - if (off < this.size and this.charAt(off) == ':') - off += 1; - - col_count += 1; - - // Pipe delimiter (optional at the end of line). */ - while (off < this.size and isWhitespace(this.charAt(off))) - off += 1; - if (off < this.size and this.charAt(off) == '|') { - delimited = true; - found_pipe = true; - off += 1; - while (off < this.size and isWhitespace(this.charAt(off))) - off += 1; - } - - // Success, if we reach end of line. - if (off >= this.size or this.isNewline(off)) - break; - - if (!delimited) - return false; - } - - if (!found_pipe) - return false; - - column_column.* = col_count; - end.* = off; - return true; - } - - fn isOpeningCodeFence(this: *MDParser, beg: u8, end: *u32) bool { - var off = beg; - const first = this.charAt(beg); - - while (off < this.size and this.charAt(off) == first) { - off += 1; - } - - // Fence must have at least three characters. - if (off - beg < 3) - return false; - - // Optionally, space(s) can follow - while (off < this.size and this.charAt(off) == ' ') { - off += 1; - } - - // Optionally, an info string can follow. - while (off < this.size and !this.isNewline(this.charAt(off))) { - // Backtick-based fence must not contain '`' in the info string. - if (first == '`' and this.charAt(off) == '`') - return false; - off += 1; - } - - end.* = off; - return true; - } - - fn isClosingCodeFence(this: *MDParser, ch: u8, beg: u8, end: *u32) bool { - var off = beg; - - defer { - end.* = off; - } - - while (off < this.size and this.charAt(off) == ch) { - off += 1; - } - - if (off - beg < this.code_fence_length) { - return false; - } - - // Optionally, space(s) can follow - while (off < this.size and this.charAt(off) == ' ') { - off += 1; - } - - // But nothing more is allowed on the line. - if (off < this.size and !this.isNewline(this.charAt(off))) - return false; - - return true; - } - - pub fn parse(this: *MDParser) anyerror!void { - var pivot_line = &Line.Analysis.blank; - var line_buf: [2]Line.Analysis = undefined; - var line = &line_buf[0]; - var offset: u32 = 0; - - try this.mdx.onEnterBlock(.doc, void, {}); - - const len: u32 = this.size; - while (offset < len) { - if (line == pivot_line) { - line = if (line == &line_buf[0]) &line_buf[1] else &line_buf[0]; - } - - try this.analyzeLine(offset, &offset, pivot_line, line); - try this.processLine(&pivot_line, line); - } - - this.endCurrentBlock(); - - try this.buildRefDefHashTable(); - - this.leaveChildContainers(0); - this.processAllBlocks(); - try this.mdx.onLeaveBlock(.doc, void, {}); - } -}; - -pub const MDX = struct { - parser: JSParser, - log: *logger.Log, - allocator: std.mem.Allocator, - stmts: std.ArrayListUnmanaged(js_ast.Stmt) = .{}, - - pub const Options = struct {}; - - pub fn onEnterBlock(this: *MDX, tag: Block.Tag, comptime Detail: type, detail: Detail) anyerror!void { - _ = tag; - _ = detail; - _ = this; - } - - pub fn onLeaveBlock(this: *MDX, tag: Block.Tag, comptime Detail: type, detail: Detail) anyerror!void { - _ = tag; - _ = detail; - _ = this; - } - - pub fn onEnterSpan(this: *MDX, tag: Span.Tag, comptime Detail: type, detail: Detail) anyerror!void { - _ = tag; - _ = detail; - _ = this; - } - - pub fn onLeaveSpan(this: *MDX, tag: Span.Tag, comptime Detail: type, detail: Detail) anyerror!void { - _ = tag; - _ = detail; - _ = this; - } - - pub fn onText(this: *MDX, tag: Text, text: []const u8) anyerror!void { - _ = tag; - _ = text; - _ = this; - } - - pub inline fn source(p: *const MDX) *const logger.Source { - return &p.lexer.source; - } - - pub fn e(_: *MDX, t: anytype, loc: logger.Loc) Expr { - const Type = @TypeOf(t); - if (@typeInfo(Type) == .Pointer) { - return Expr.init(std.meta.Child(Type), t.*, loc); - } else { - return Expr.init(Type, t, loc); - } - } - - pub fn s(_: *MDX, t: anytype, loc: logger.Loc) Stmt { - const Type = @TypeOf(t); - if (@typeInfo(Type) == .Pointer) { - return Stmt.init(std.meta.Child(Type), t.*, loc); - } else { - return Stmt.alloc(Type, t, loc); - } - } - - pub fn setup( - this: *MDX, - _options: ParserOptions, - log: *logger.Log, - source_: *const logger.Source, - define: *Define, - allocator: std.mem.Allocator, - ) !void { - try JSParser.init( - allocator, - log, - source_, - define, - js_lexer.Lexer.initNoAutoStep(log, source_.*, allocator), - _options, - &this.parser, - ); - this.lexer = try Lexer.init(&this.parser.lexer); - this.allocator = allocator; - this.log = log; - this.stmts = .{}; - } - - pub fn parse(this: *MDX) !js_ast.Result { - try this._parse(); - return try runVisitPassAndFinish(JSParser, &this.parser, try this.stmts.toOwnedSlice(this.allocator)); - } - - fn run(this: *MDX) anyerror!logger.Loc { - _ = this; - return logger.Loc.Empty; - } - - fn _parse(this: *MDX) anyerror!void { - var root_children = std.ArrayListUnmanaged(Expr){}; - var first_loc = try run(this, &root_children); - - first_loc.start = @max(first_loc.start, 0); - const args_loc = first_loc; - first_loc.start += 1; - const body_loc = first_loc; - - // We need to simulate a function that was parsed - _ = try this.parser.pushScopeForParsePass(.function_args, args_loc); - - _ = try this.parser.pushScopeForParsePass(.function_body, body_loc); - - const root = this.e(E.JSXElement{ - .tag = this.e(E.JSXElement.Tag.map.get(E.JSXElement.Tag.main), body_loc), - .children = ExprNodeList.fromList(root_children), - }, body_loc); - - var root_stmts = try this.allocator.alloc(Stmt, 1); - root_stmts[0] = this.s(S.Return{ .value = root }, body_loc); - - try this.stmts.append( - this.allocator, - - this.s(S.ExportDefault{ - .default_name = try this.parser.createDefaultName(args_loc), - .value = .{ - .expr = this.e(E.Arrow{ - .body = G.FnBody{ - .stmts = root_stmts, - .loc = body_loc, - }, - .args = &[_]G.Arg{}, - .prefer_expr = true, - }, args_loc), - }, - }, args_loc), - ); - } -}; diff --git a/src/options.zig b/src/options.zig index f30594516..3e7eec72a 100644 --- a/src/options.zig +++ b/src/options.zig @@ -1989,6 +1989,7 @@ pub const OutputFile = struct { input: Fs.Path, value: Value, size: usize = 0, + size_without_sourcemap: usize = 0, mtime: ?i128 = null, hash: u64 = 0, source_map_index: u32 = std.math.maxInt(u32), @@ -2099,6 +2100,7 @@ pub const OutputFile = struct { output_path: string, size: ?usize = null, input_path: []const u8 = "", + display_size: u32 = 0, output_kind: JSC.API.BuildArtifact.OutputKind = .chunk, data: union(enum) { buffer: struct { @@ -2125,6 +2127,7 @@ pub const OutputFile = struct { .file => |file| file.size, .saved => 0, }, + .size_without_sourcemap = options.display_size, .hash = options.hash orelse 0, .output_kind = options.output_kind, .source_map_index = options.source_map_index orelse std.math.maxInt(u32), diff --git a/src/output.zig b/src/output.zig index 0b0780bf8..4f47d2496 100644 --- a/src/output.zig +++ b/src/output.zig @@ -317,6 +317,22 @@ pub fn printElapsedStdout(elapsed: f64) void { printElapsedToWithCtx(elapsed, Output.pretty, false, {}); } +pub fn printElapsedStdoutTrim(elapsed: f64) void { + switch (@floatToInt(i64, @round(elapsed))) { + 0...1500 => { + const fmt = "<r><d>[<b>{d:>}ms<r><d>]<r>"; + const args = .{elapsed}; + pretty(fmt, args); + }, + else => { + const fmt = "<r><d>[<b>{d:>}s<r><d>]<r>"; + const args = .{elapsed / 1000.0}; + + pretty(fmt, args); + }, + } +} + pub fn printStartEnd(start: i128, end: i128) void { const elapsed = @divTrunc(@truncate(i64, end - start), @as(i64, std.time.ns_per_ms)); printElapsed(@intToFloat(f64, elapsed)); diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 01c37c632..58f00c2af 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -167,6 +167,8 @@ pub const Result = struct { is_external: bool = false, + is_standalone_module: bool = false, + // This is true when the package was loaded from within the node_modules directory. is_from_node_modules: bool = false, @@ -485,6 +487,8 @@ pub const Resolver = struct { env_loader: ?*DotEnv.Loader = null, store_fd: bool = false, + standalone_module_graph: ?*bun.StandaloneModuleGraph = null, + // These are sets that represent various conditions for the "exports" field // in package.json. // esm_conditions_default: bun.StringHashMap(bool), @@ -814,6 +818,23 @@ pub const Resolver = struct { }; } + if (r.standalone_module_graph) |graph| { + if (strings.hasPrefixComptime(import_path, "compiled://")) { + if (graph.files.contains(import_path)) { + return .{ + .success = Result{ + .import_kind = kind, + .path_pair = PathPair{ + .primary = Path.init(import_path), + }, + .is_standalone_module = true, + .module_type = .esm, + }, + }; + } + } + } + if (DataURL.parse(import_path)) |_data_url| { const data_url: DataURL = _data_url; // "import 'data:text/javascript,console.log(123)';" @@ -862,7 +883,7 @@ pub const Resolver = struct { var tmp = r.resolveWithoutSymlinks(source_dir, import_path, kind, global_cache); switch (tmp) { .success => |*result| { - if (!strings.eqlComptime(result.path_pair.primary.namespace, "node")) + if (!strings.eqlComptime(result.path_pair.primary.namespace, "node") and !result.is_standalone_module) r.finalizeResult(result, kind) catch |err| return .{ .failure = err }; r.flushDebugLogs(.success) catch {}; diff --git a/src/runtime.zig b/src/runtime.zig index 96a51699e..99d8ca102 100644 --- a/src/runtime.zig +++ b/src/runtime.zig @@ -138,7 +138,7 @@ pub const Fallback = struct { return ProdSourceContent; } } - pub const version_hash = @embedFile("./fallback.version"); + pub const version_hash = @import("build_options").fallback_html_version; var version_hash_int: u32 = 0; pub fn versionHash() u32 { if (version_hash_int == 0) { @@ -263,26 +263,15 @@ pub const Runtime = struct { } } - pub const version_hash = @embedFile("./runtime.version"); + pub const version_hash = @import("build_options").runtime_js_version; var version_hash_int: u32 = 0; pub fn versionHash() u32 { if (version_hash_int == 0) { - version_hash_int = @truncate(u32, std.fmt.parseInt(u64, version(), 16) catch unreachable); + version_hash_int = @truncate(u32, version_hash); } return version_hash_int; } - pub inline fn version() string { - return version_hash; - } - - const bytecodeCacheFilename = std.fmt.comptimePrint("__runtime.{s}", .{version_hash}); - var bytecodeCacheFetcher = Fs.BytecodeCacheFetcher{}; - - pub fn byteCodeCacheFile(fs: *Fs.FileSystem.RealFS) ?bun.StoredFileDescriptorType { - return bytecodeCacheFetcher.fetch(bytecodeCacheFilename, fs); - } - pub const Features = struct { react_fast_refresh: bool = false, hot_module_reloading: bool = false, diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig index cc557caf1..d9dd85a7e 100644 --- a/src/sourcemap/sourcemap.zig +++ b/src/sourcemap/sourcemap.zig @@ -35,10 +35,84 @@ pub const SourceMapState = struct { }; sources: [][]const u8 = &[_][]u8{}, -sources_content: [][]SourceContent, +sources_content: []string, mapping: Mapping.List = .{}, allocator: std.mem.Allocator, +pub fn parse( + allocator: std.mem.Allocator, + json_source: *const Logger.Source, + log: *Logger.Log, +) !SourceMap { + var json = try bun.JSON.ParseJSONUTF8(json_source, log, allocator); + var mappings = bun.sourcemap.Mapping.List{}; + + if (json.get("version")) |version| { + if (version.data != .e_number or version.data.e_number.value != 3.0) { + return error.@"Unsupported sourcemap version"; + } + } + + if (json.get("mappings")) |mappings_str| { + if (mappings_str.data != .e_string) { + return error.@"Invalid sourcemap mappings"; + } + + var parsed = bun.sourcemap.Mapping.parse(allocator, try mappings_str.data.e_string.toUTF8(allocator), null, std.math.maxInt(i32)); + if (parsed == .fail) { + try log.addMsg(bun.logger.Msg{ + .data = parsed.fail.toData("sourcemap.json"), + .kind = .err, + }); + return error.@"Failed to parse sourcemap mappings"; + } + + mappings = parsed.success; + } + + var sources = std.ArrayList(bun.string).init(allocator); + var sources_content = std.ArrayList(string).init(allocator); + + if (json.get("sourcesContent")) |mappings_str| { + if (mappings_str.data != .e_array) { + return error.@"Invalid sourcemap sources"; + } + + try sources_content.ensureTotalCapacityPrecise(mappings_str.data.e_array.items.len); + for (mappings_str.data.e_array.items.slice()) |source| { + if (source.data != .e_string) { + return error.@"Invalid sourcemap source"; + } + + try source.data.e_string.toUTF8(allocator); + sources_content.appendAssumeCapacity(source.data.e_string.slice()); + } + } + + if (json.get("sources")) |mappings_str| { + if (mappings_str.data != .e_array) { + return error.@"Invalid sourcemap sources"; + } + + try sources.ensureTotalCapacityPrecise(mappings_str.data.e_array.items.len); + for (mappings_str.data.e_array.items.slice()) |source| { + if (source.data != .e_string) { + return error.@"Invalid sourcemap source"; + } + + try source.data.e_string.toUTF8(allocator); + sources.appendAssumeCapacity(source.data.e_string.slice()); + } + } + + return SourceMap{ + .mapping = mappings, + .allocator = allocator, + .sources_content = sources_content.items, + .sources = sources.items, + }; +} + pub const Mapping = struct { generated: LineColumnOffset, original: LineColumnOffset, diff --git a/src/standalone_bun.zig b/src/standalone_bun.zig new file mode 100644 index 000000000..332bf5dad --- /dev/null +++ b/src/standalone_bun.zig @@ -0,0 +1,480 @@ +// Originally, we tried using LIEF to inject the module graph into a MachO segment +// But this incurred a fixed 350ms overhead on every build, which is unacceptable +// so we give up on codesigning support on macOS for now until we can find a better solution +const bun = @import("root").bun; +const std = @import("std"); +const Schema = bun.Schema.Api; + +const Environment = bun.Environment; + +pub const StandaloneModuleGraph = struct { + bytes: []const u8 = "", + files: bun.StringArrayHashMap(File), + entry_point_id: u32 = 0, + + pub fn entryPoint(this: *const StandaloneModuleGraph) *File { + return &this.files.values()[this.entry_point_id]; + } + + pub const CompiledModuleGraphFile = struct { + name: Schema.StringPointer = .{}, + loader: bun.options.Loader = .file, + contents: Schema.StringPointer = .{}, + sourcemap: Schema.StringPointer = .{}, + }; + + pub const File = struct { + name: []const u8 = "", + loader: bun.options.Loader, + contents: []const u8 = "", + sourcemap: LazySourceMap, + }; + + pub const LazySourceMap = union(enum) { + compressed: []const u8, + decompressed: bun.sourcemap, + + pub fn load(this: *LazySourceMap, log: *bun.logger.Log, allocator: std.mem.Allocator) !*bun.sourcemap { + if (this.* == .decompressed) return &this.decompressed; + + var decompressed = try allocator.alloc(u8, bun.zstd.getDecompressedSize(this.compressed)); + var result = bun.zstd.decompress(decompressed, this.compressed); + if (result == .err) { + allocator.free(decompressed); + log.addError(null, bun.logger.Loc.Empty, bun.span(result.err)) catch unreachable; + return error.@"Failed to decompress sourcemap"; + } + errdefer allocator.free(decompressed); + var bytes = decompressed[0..result.success]; + + this.* = .{ .decompressed = try bun.sourcemap.parse(allocator, &bun.logger.Source.initPathString("sourcemap.json", bytes), log) }; + return &this.decompressed; + } + }; + + pub const Offsets = extern struct { + byte_count: usize = 0, + modules_ptr: bun.StringPointer = .{}, + entry_point_id: u32 = 0, + }; + + const trailer = "\n---- Bun! ----\n"; + + pub fn fromBytes(allocator: std.mem.Allocator, raw_bytes: []const u8, offsets: Offsets) !StandaloneModuleGraph { + if (raw_bytes.len == 0) return StandaloneModuleGraph{ + .files = bun.StringArrayHashMap(File).init(allocator), + }; + + const modules_list_bytes = sliceTo(raw_bytes, offsets.modules_ptr); + const modules_list = std.mem.bytesAsSlice(CompiledModuleGraphFile, modules_list_bytes); + + if (offsets.entry_point_id > modules_list.len) { + return error.@"Corrupted module graph: entry point ID is greater than module list count"; + } + + var modules = bun.StringArrayHashMap(File).init(allocator); + try modules.ensureTotalCapacity(modules_list.len); + for (modules_list) |module| { + modules.putAssumeCapacity( + sliceTo(raw_bytes, module.name), + File{ + .name = sliceTo(raw_bytes, module.name), + .loader = module.loader, + .contents = sliceTo(raw_bytes, module.contents), + .sourcemap = LazySourceMap{ + .compressed = sliceTo(raw_bytes, module.sourcemap), + }, + }, + ); + } + + return StandaloneModuleGraph{ + .bytes = raw_bytes[0..offsets.byte_count], + .files = modules, + .entry_point_id = offsets.entry_point_id, + }; + } + + fn sliceTo(bytes: []const u8, ptr: bun.StringPointer) []const u8 { + if (ptr.length == 0) return ""; + + return bytes[ptr.offset..][0..ptr.length]; + } + + pub fn toBytes(allocator: std.mem.Allocator, prefix: []const u8, output_files: []const bun.options.OutputFile) ![]u8 { + var serialize_trace = bun.tracy.traceNamed(@src(), "ModuleGraph.serialize"); + defer serialize_trace.end(); + var entry_point_id: ?usize = null; + var string_builder = bun.StringBuilder{}; + var module_count: usize = 0; + for (output_files, 0..) |output_file, i| { + string_builder.count(output_file.path); + string_builder.count(prefix); + if (output_file.value == .buffer) { + if (output_file.output_kind == .sourcemap) { + string_builder.cap += bun.zstd.compressBound(output_file.value.buffer.bytes.len); + } else { + if (entry_point_id == null) { + if (output_file.output_kind == .@"entry-point") { + entry_point_id = i; + } + } + + string_builder.count(output_file.value.buffer.bytes); + module_count += 1; + } + } + } + + if (module_count == 0 or entry_point_id == null) return &[_]u8{}; + + string_builder.cap += @sizeOf(CompiledModuleGraphFile) * output_files.len; + string_builder.cap += trailer.len; + string_builder.cap += 16; + + { + var offsets_ = Offsets{}; + string_builder.cap += std.mem.asBytes(&offsets_).len; + } + + try string_builder.allocate(allocator); + + var modules = try std.ArrayList(CompiledModuleGraphFile).initCapacity(allocator, module_count); + + for (output_files) |output_file| { + if (output_file.output_kind == .sourcemap) { + continue; + } + + if (output_file.value != .buffer) { + continue; + } + + var module = CompiledModuleGraphFile{ + .name = string_builder.fmtAppendCount("{s}{s}", .{ prefix, output_file.path }), + .loader = output_file.loader, + .contents = string_builder.appendCount(output_file.value.buffer.bytes), + }; + if (output_file.source_map_index != std.math.maxInt(u32)) { + var remaining_slice = string_builder.allocatedSlice()[string_builder.len..]; + const compressed_result = bun.zstd.compress(remaining_slice, output_files[output_file.source_map_index].value.buffer.bytes, 1); + if (compressed_result == .err) { + bun.Output.panic("Unexpected error compressing sourcemap: {s}", .{bun.span(compressed_result.err)}); + } + module.sourcemap = string_builder.add(compressed_result.success); + } + modules.appendAssumeCapacity(module); + } + + var offsets = Offsets{ + .entry_point_id = @truncate(u32, entry_point_id.?), + .modules_ptr = string_builder.appendCount(std.mem.sliceAsBytes(modules.items)), + .byte_count = string_builder.len, + }; + + _ = string_builder.append(std.mem.asBytes(&offsets)); + _ = string_builder.append(trailer); + + return string_builder.ptr.?[0..string_builder.len]; + } + + const page_size = if (Environment.isLinux and Environment.isAarch64) + // some linux distros do 64 KB pages on aarch64 + 64 * 1024 + else + std.mem.page_size; + + pub fn inject(bytes: []const u8) i32 { + var buf: [512]u8 = undefined; + var zname = bun.span(bun.fs.FileSystem.instance.tmpname("bun-build", &buf, @bitCast(u64, std.time.milliTimestamp())) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get temporary file name: {s}", .{@errorName(err)}); + Global.exit(1); + return -1; + }); + + const cloned_executable_fd: bun.FileDescriptor = brk: { + var self_buf: [bun.MAX_PATH_BYTES + 1]u8 = undefined; + var self_exe = std.fs.selfExePath(&self_buf) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get self executable path: {s}", .{@errorName(err)}); + Global.exit(1); + return -1; + }; + self_buf[self_exe.len] = 0; + var self_exeZ = self_buf[0..self_exe.len :0]; + + if (comptime Environment.isMac) { + // if we're on a mac, use clonefile() if we can + // failure is okay, clonefile is just a fast path. + if (bun.C.darwin.clonefile(self_exeZ.ptr, zname.ptr, 0) == 0) { + switch (bun.JSC.Node.Syscall.open(zname, std.os.O.WRONLY | std.os.O.CLOEXEC, 0)) { + .result => |res| break :brk res, + .err => {}, + } + } + } + + // otherwise, just copy the file + const fd = switch (bun.JSC.Node.Syscall.open(zname, std.os.O.CLOEXEC | std.os.O.RDONLY, 0)) { + .result => |res| res, + .err => |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to open temporary file to copy bun into: {s}", .{err.toSystemError().message.slice()}); + Global.exit(1); + }, + }; + const self_fd = switch (bun.JSC.Node.Syscall.open(self_exeZ, std.os.O.CLOEXEC | std.os.O.WRONLY | std.os.O.CREAT, 0)) { + .result => |res| res, + .err => |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to open bun executable to copy from as read-only: {s}", .{err.toSystemError().message.slice()}); + Global.exit(1); + }, + }; + defer _ = bun.JSC.Node.Syscall.close(self_fd); + bun.copyFile(self_fd, fd) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to copy bun executable into temporary file: {s}", .{@errorName(err)}); + Global.exit(1); + }; + break :brk fd; + }; + + // Always leave at least one full page of padding at the end of the file. + const total_byte_count = brk: { + const fstat = std.os.fstat(cloned_executable_fd) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to stat temporary file: {s}", .{@errorName(err)}); + Global.exit(1); + }; + + const count = @intCast(usize, @max(fstat.size, 0) + page_size + @intCast(i64, bytes.len) + 8); + + std.os.lseek_SET(cloned_executable_fd, 0) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to seek to end of temporary file: {s}", .{@errorName(err)}); + Global.exit(1); + }; + + // grow it by one page + the size of the module graph + std.os.ftruncate(cloned_executable_fd, count) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to truncate temporary file: {s}", .{@errorName(err)}); + Global.exit(1); + }; + break :brk count; + }; + + std.os.lseek_END(cloned_executable_fd, -@intCast(i64, bytes.len + 8)) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to seek to end of temporary file: {s}", .{@errorName(err)}); + Global.exit(1); + }; + + var remain = bytes; + while (remain.len > 0) { + switch (bun.JSC.Node.Syscall.write(cloned_executable_fd, bytes)) { + .result => |written| remain = remain[written..], + .err => |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to write to temporary file: {s}", .{err.toSystemError().message.slice()}); + Global.exit(1); + }, + } + } + + // the final 8 bytes in the file are the length of the module graph with padding, excluding the trailer and offsets + _ = bun.JSC.Node.Syscall.write(cloned_executable_fd, std.mem.asBytes(&total_byte_count)); + + _ = bun.C.fchmod(cloned_executable_fd, 0o777); + + return cloned_executable_fd; + } + + pub fn toExecutable(allocator: std.mem.Allocator, output_files: []const bun.options.OutputFile, root_dir: std.fs.IterableDir, module_prefix: []const u8, outfile: []const u8) !void { + const bytes = try toBytes(allocator, module_prefix, output_files); + if (bytes.len == 0) return; + + const fd = inject(bytes); + if (fd == -1) { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to inject into file", .{}); + Global.exit(1); + } + + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const temp_location = bun.getFdPath(fd, &buf) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to get path for fd: {s}", .{@errorName(err)}); + Global.exit(1); + }; + + if (comptime Environment.isMac) { + { + var signer = std.ChildProcess.init( + &.{ + "codesign", + "--remove-signature", + temp_location, + }, + bun.default_allocator, + ); + if (bun.logger.Log.default_log_level.atLeast(.verbose)) { + signer.stdout_behavior = .Inherit; + signer.stderr_behavior = .Inherit; + signer.stdin_behavior = .Inherit; + } else { + signer.stdout_behavior = .Ignore; + signer.stderr_behavior = .Ignore; + signer.stdin_behavior = .Ignore; + } + _ = signer.spawnAndWait() catch {}; + } + } + + std.os.renameat(std.fs.cwd().fd, temp_location, root_dir.dir.fd, outfile) catch |err| { + Output.prettyErrorln("<r><red>error<r><d>:<r> failed to rename {s} to {s}: {s}", .{ temp_location, outfile, @errorName(err) }); + Global.exit(1); + }; + } + + pub fn fromExecutable(allocator: std.mem.Allocator) !?StandaloneModuleGraph { + const self_exe = (openSelfExe(.{}) catch null) orelse return null; + defer _ = bun.JSC.Node.Syscall.close(self_exe); + + var trailer_bytes: [4096]u8 = undefined; + std.os.lseek_END(self_exe, -4096) catch return null; + var read_amount: usize = 0; + while (read_amount < trailer_bytes.len) { + switch (bun.JSC.Node.Syscall.read(self_exe, trailer_bytes[read_amount..])) { + .result => |read| { + if (read == 0) return null; + + read_amount += read; + }, + .err => { + return null; + }, + } + } + + if (read_amount < trailer.len + @sizeOf(usize) + 32) + // definitely missing data + return null; + + var end = @as([]u8, &trailer_bytes).ptr + read_amount - @sizeOf(usize); + const total_byte_count: usize = @bitCast(usize, end[0..8].*); + + if (total_byte_count > std.math.maxInt(u32) or total_byte_count < 4096) { + // sanity check: the total byte count should never be more than 4 GB + // bun is at least like 30 MB so if it reports a size less than 4096 bytes then something is wrong + return null; + } + end -= trailer.len; + + if (!bun.strings.hasPrefixComptime(end[0..trailer.len], trailer)) { + // invalid trailer + return null; + } + + end -= @sizeOf(Offsets); + + const offsets: Offsets = std.mem.bytesAsValue(Offsets, end[0..@sizeOf(Offsets)]).*; + if (offsets.byte_count >= total_byte_count) { + // if we hit this branch then the file is corrupted and we should just give up + return null; + } + + var to_read = try bun.default_allocator.alloc(u8, offsets.byte_count); + var to_read_from = to_read; + + // Reading the data and making sure it's page-aligned + won't crash due + // to out of bounds using mmap() is very complicated. + // So even though we ensure there is at least one page of padding at the end of the file, + // we just read the whole thing into memory for now. + // at the very least + // if you have not a ton of code, we only do a single read() call + if (Environment.allow_assert or offsets.byte_count > 1024 * 3) { + const offset_from_end = trailer_bytes.len - (@ptrToInt(end) - @ptrToInt(@as([]u8, &trailer_bytes).ptr)); + std.os.lseek_END(self_exe, -@intCast(i64, offset_from_end + offsets.byte_count)) catch return null; + + if (comptime Environment.allow_assert) { + // actually we just want to verify this logic is correct in development + if (offsets.byte_count <= 1024 * 3) { + to_read_from = try bun.default_allocator.alloc(u8, offsets.byte_count); + } + } + + var remain = to_read_from; + while (remain.len > 0) { + switch (bun.JSC.Node.Syscall.read(self_exe, remain)) { + .result => |read| { + if (read == 0) return null; + + remain = remain[read..]; + }, + .err => { + bun.default_allocator.free(to_read); + return null; + }, + } + } + } + + if (offsets.byte_count <= 1024 * 3) { + // we already have the bytes + end -= offsets.byte_count; + @memcpy(to_read.ptr, end, offsets.byte_count); + if (comptime Environment.allow_assert) { + std.debug.assert(bun.strings.eqlLong(to_read, end[0..offsets.byte_count], true)); + } + } + + return try StandaloneModuleGraph.fromBytes(allocator, to_read, offsets); + } + + // this is based on the Zig standard library function, except it accounts for + fn openSelfExe(flags: std.fs.File.OpenFlags) std.fs.OpenSelfExeError!?bun.FileDescriptor { + // heuristic: `bun build --compile` won't be supported if the name is "bun" or "bunx". + // this is a cheap way to avoid the extra overhead of opening the executable + // and also just makes sense. + if (std.os.argv.len > 0) { + const argv0_len = bun.len(std.os.argv[0]); + if (argv0_len == 3) { + if (bun.strings.eqlComptimeIgnoreLen(std.os.argv[0][0..argv0_len], "bun")) { + return null; + } + } + + if (argv0_len == 4) { + if (bun.strings.eqlComptimeIgnoreLen(std.os.argv[0][0..argv0_len], "bunx")) { + return null; + } + } + } + + if (comptime Environment.isLinux) { + if (std.fs.openFileAbsoluteZ("/proc/self/exe", flags)) |easymode| { + return easymode.handle; + } else |_| { + if (std.os.argv.len > 0) { + // The user doesn't have /proc/ mounted, so now we just guess and hope for the best. + var whichbuf: [bun.MAX_PATH_BYTES]u8 = undefined; + if (bun.which( + &whichbuf, + bun.getenvZ("PATH") orelse return error.FileNotFound, + "", + bun.span(std.os.argv[0]), + )) |path| { + return (try std.fs.cwd().openFileZ(path, flags)).handle; + } + } + + return error.FileNotFound; + } + } + + if (comptime Environment.isWindows) { + return (try std.fs.openSelfExe(flags)).handle; + } + // Use of MAX_PATH_BYTES here is valid as the resulting path is immediately + // opened with no modification. + var buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const self_exe_path = try std.fs.selfExePath(&buf); + buf[self_exe_path.len] = 0; + const file = try std.fs.openFileAbsoluteZ(buf[0..self_exe_path.len :0].ptr, flags); + return file.handle; + } +}; + +const Output = bun.Output; +const Global = bun.Global; diff --git a/src/string_builder.zig b/src/string_builder.zig index abed901dd..7aba5cd89 100644 --- a/src/string_builder.zig +++ b/src/string_builder.zig @@ -53,6 +53,36 @@ pub fn append(this: *StringBuilder, slice: string) string { return result; } +pub fn add(this: *StringBuilder, len: usize) bun.StringPointer { + if (comptime Environment.allow_assert) { + assert(this.len <= this.cap); // didn't count everything + assert(this.ptr != null); // must call allocate first + } + + const start = this.len; + this.len += len; + + if (comptime Environment.allow_assert) assert(this.len <= this.cap); + + return bun.StringPointer{ .offset = @truncate(u32, start), .length = @truncate(u32, len) }; +} +pub fn appendCount(this: *StringBuilder, slice: string) bun.StringPointer { + if (comptime Environment.allow_assert) { + assert(this.len <= this.cap); // didn't count everything + assert(this.ptr != null); // must call allocate first + } + + const start = this.len; + bun.copy(u8, this.ptr.?[this.len..this.cap], slice); + const result = this.ptr.?[this.len..this.cap][0..slice.len]; + _ = result; + this.len += slice.len; + + if (comptime Environment.allow_assert) assert(this.len <= this.cap); + + return bun.StringPointer{ .offset = @truncate(u32, start), .length = @truncate(u32, slice.len) }; +} + pub fn fmt(this: *StringBuilder, comptime str: string, args: anytype) string { if (comptime Environment.allow_assert) { assert(this.len <= this.cap); // didn't count everything @@ -68,6 +98,25 @@ pub fn fmt(this: *StringBuilder, comptime str: string, args: anytype) string { return out; } +pub fn fmtAppendCount(this: *StringBuilder, comptime str: string, args: anytype) bun.StringPointer { + if (comptime Environment.allow_assert) { + assert(this.len <= this.cap); // didn't count everything + assert(this.ptr != null); // must call allocate first + } + + var buf = this.ptr.?[this.len..this.cap]; + const out = std.fmt.bufPrint(buf, str, args) catch unreachable; + const off = this.len; + this.len += out.len; + + if (comptime Environment.allow_assert) assert(this.len <= this.cap); + + return bun.StringPointer{ + .offset = @truncate(u32, off), + .length = @truncate(u32, out.len), + }; +} + pub fn fmtCount(this: *StringBuilder, comptime str: string, args: anytype) void { this.cap += std.fmt.count(str, args); } @@ -79,3 +128,11 @@ pub fn allocatedSlice(this: *StringBuilder) []u8 { } return ptr[0..this.cap]; } + +pub fn writable(this: *StringBuilder) []u8 { + var ptr = this.ptr orelse return &[_]u8{}; + if (comptime Environment.allow_assert) { + assert(this.cap > 0); + } + return ptr[this.len..this.cap]; +} diff --git a/src/which.zig b/src/which.zig index 1cef93a22..8c29e7e4a 100644 --- a/src/which.zig +++ b/src/which.zig @@ -33,8 +33,10 @@ pub fn which(buf: *[bun.MAX_PATH_BYTES]u8, path: []const u8, cwd: []const u8, bi // /foo/bar/baz as a path and you're in /home/jarred? } - if (isValid(buf, std.mem.trimRight(u8, cwd, std.fs.path.sep_str), bin)) |len| { - return buf[0..len :0]; + if (cwd.len > 0) { + if (isValid(buf, std.mem.trimRight(u8, cwd, std.fs.path.sep_str), bin)) |len| { + return buf[0..len :0]; + } } var path_iter = std.mem.tokenize(u8, path, ":"); diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 4960c7d39..c654602bc 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -51,7 +51,7 @@ describe("bundler", () => { }); itBundled("edgecase/BunPluginTreeShakeImport", { notImplemented: true, - // This only appears at runtime and not with bun build, even with --transpile + // This only appears at runtime and not with bun build, even with --no-bundle files: { "/entry.ts": /* js */ ` import { A, B } from "./somewhere-else"; diff --git a/test/bundler/expectBundled.ts b/test/bundler/expectBundled.ts index 471cdd7f2..0d0680efe 100644 --- a/test/bundler/expectBundled.ts +++ b/test/bundler/expectBundled.ts @@ -521,7 +521,7 @@ function expectBundled( "build", ...entryPaths, ...(entryPointsRaw ?? []), - bundling === false ? "--transpile" : [], + bundling === false ? "--no-bundle" : [], outfile ? `--outfile=${outfile}` : `--outdir=${outdir}`, define && Object.entries(define).map(([k, v]) => ["--define", `${k}=${v}`]), `--target=${target}`, |