diff --git a/.github/workflows/bun-windows.yml b/.github/workflows/bun-windows.yml index 6d26283716..e74f2843c8 100644 --- a/.github/workflows/bun-windows.yml +++ b/.github/workflows/bun-windows.yml @@ -178,6 +178,11 @@ jobs: path: bun-deps key: ${{ steps.cache-deps-restore.outputs.cache-primary-key }} + # TODO(@paperdave): stop relying on this and use bun.exe to build itself. + # we cant do that now because there isn't a tagged release to use. + # + # and at the time of writing, the minimum canary required to work is not + # yet released as it is the one *this* commit. windows-codegen: name: Codegen runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index d7536be60a..d7df4a4bae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -387,10 +387,11 @@ elseif(NOT BUN_CPP_ONLY AND NOT BUN_LINK_ONLY) endif() # Bun -if(NOT WIN32) - find_program(BUN_EXECUTABLE bun ${REQUIRED_IF_NOT_ONLY_CPP_OR_LINK} DOC "Path to an already built release of Bun") - message(STATUS "Found Bun: ${BUN_EXECUTABLE}") -else() +find_program(BUN_EXECUTABLE bun ${REQUIRED_IF_NOT_ONLY_CPP_OR_LINK} DOC "Path to an already built release of Bun") +message(STATUS "Found Bun: ${BUN_EXECUTABLE}") + +if(WIN32 AND NO_CODEGEN) + # TODO(@paperdave): remove this, see bun-windows.yml's comment. set(BUN_EXECUTABLE "echo") endif() @@ -601,7 +602,7 @@ add_custom_command( "${BUN_WORKDIR}/codegen/ZigGeneratedClasses+DOMIsoSubspaces.h" "${BUN_WORKDIR}/codegen/ZigGeneratedClasses+lazyStructureImpl.h" "${BUN_WORKDIR}/codegen/ZigGeneratedClasses.zig" - COMMAND ${BUN_EXECUTABLE} "${BUN_CODEGEN_SRC}/generate-classes.ts" ${BUN_CLASSES_TS} "${BUN_WORKDIR}/codegen" + COMMAND ${BUN_EXECUTABLE} run "${BUN_CODEGEN_SRC}/generate-classes.ts" ${BUN_CLASSES_TS} "${BUN_WORKDIR}/codegen" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} MAIN_DEPENDENCY "${BUN_CODEGEN_SRC}/generate-classes.ts" DEPENDS ${BUN_CLASSES_TS} @@ -614,11 +615,12 @@ list(APPEND BUN_RAW_SOURCES "${BUN_WORKDIR}/codegen/ZigGeneratedClasses.cpp") add_custom_command( OUTPUT "${BUN_WORKDIR}/codegen/JSSink.cpp" "${BUN_WORKDIR}/codegen/JSSink.h" - COMMAND ${BUN_EXECUTABLE} "src/codegen/generate-jssink.ts" "${BUN_WORKDIR}/codegen" + COMMAND ${BUN_EXECUTABLE} run "src/codegen/generate-jssink.ts" "${BUN_WORKDIR}/codegen" VERBATIM MAIN_DEPENDENCY "src/codegen/generate-jssink.ts" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Generating JSSink" + USES_TERMINAL ) list(APPEND BUN_RAW_SOURCES "${BUN_WORKDIR}/codegen/JSSink.cpp") @@ -641,7 +643,7 @@ if(NOT BUN_LINK_ONLY) OUTPUT ${_output} MAIN_DEPENDENCY ${BUN_HASH_LUT_GENERATOR} DEPENDS ${_input} - COMMAND ${BUN_EXECUTABLE} ${BUN_HASH_LUT_GENERATOR} ${_input} ${_output} + COMMAND ${BUN_EXECUTABLE} run ${BUN_HASH_LUT_GENERATOR} ${_input} ${_output} VERBATIM COMMENT "Generating ${_display_name}" ) @@ -715,7 +717,7 @@ if(NOT NO_CODEGEN) "${BUN_WORKDIR}/codegen/NativeModuleImpl.h" "${BUN_WORKDIR}/codegen/ResolvedSourceTag.zig" "${BUN_WORKDIR}/codegen/SyntheticModuleType.h" - COMMAND ${BUN_EXECUTABLE} "${BUN_SRC}/codegen/bundle-modules.ts" "--debug=${DEBUG}" "${BUN_WORKDIR}" + COMMAND ${BUN_EXECUTABLE} run "${BUN_SRC}/codegen/bundle-modules.ts" "--debug=${DEBUG}" "${BUN_WORKDIR}" DEPENDS ${BUN_TS_MODULES} ${CODEGEN_FILES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Bundling JS modules" @@ -730,7 +732,7 @@ WEBKIT_ADD_SOURCE_DEPENDENCIES( add_custom_command( OUTPUT "${BUN_WORKDIR}/codegen/WebCoreJSBuiltins.cpp" "${BUN_WORKDIR}/codegen/WebCoreJSBuiltins.h" - COMMAND ${BUN_EXECUTABLE} "${BUN_SRC}/codegen/bundle-functions.ts" "--debug=${DEBUG}" "${BUN_WORKDIR}" + COMMAND ${BUN_EXECUTABLE} run "${BUN_SRC}/codegen/bundle-functions.ts" "--debug=${DEBUG}" "${BUN_WORKDIR}" DEPENDS ${BUN_TS_FUNCTIONS} ${CODEGEN_FILES} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Bundling JS builtin functions" @@ -1141,9 +1143,10 @@ endif() if(WIN32) # Kill all instances of bun before linking. + # This is necessary because the file is locked by the process. add_custom_command( TARGET ${bun} - PRE_BUILD + PRE_LINK COMMAND "powershell" "/C" diff --git a/docs/installation.md b/docs/installation.md index 5815bc619d..32965ca5ff 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -233,6 +233,10 @@ If you need to remove Bun from your system, use the following commands. $ rm -rf ~/.bun # for macOS, Linux, and WSL ``` +```bash#Windows +$ Remove-Item ~\.bun -Recurse +``` + ```bash#NPM $ npm uninstall -g bun ``` diff --git a/docs/test/coverage.md b/docs/test/coverage.md index 4a9beeb9da..e544fb550d 100644 --- a/docs/test/coverage.md +++ b/docs/test/coverage.md @@ -52,7 +52,7 @@ It is possible to specify a coverage threshold in `bunfig.toml`. If your test su coverageThreshold = 0.9 # to set different thresholds for lines and functions -coverageThreshold = { line = 0.9, function = 0.9 } +coverageThreshold = { lines = 0.9, functions = 0.9 } ``` ### Sourcemaps diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c index 7866b6d0a9..83e9a96672 100644 --- a/packages/bun-usockets/src/eventing/epoll_kqueue.c +++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c @@ -31,7 +31,7 @@ void Bun__internal_dispatch_ready_poll(void* loop, void* poll); #include #endif -void us_loop_run_bun_tick(struct us_loop_t *loop, int64_t timeoutMs, void*); +void us_loop_run_bun_tick(struct us_loop_t *loop, int64_t timeoutMs); /* Pointer tags are used to indicate a Bun pointer versus a uSockets pointer */ #define UNSET_BITS_49_UNTIL_64 0x0000FFFFFFFFFFFF @@ -175,11 +175,7 @@ void us_loop_run(struct us_loop_t *loop) { } } -void bun_on_tick_before(void* ctx); -void bun_on_tick_after(void* ctx); - - -void us_loop_run_bun_tick(struct us_loop_t *loop, int64_t timeoutMs, void* tickCallbackContext) { +void us_loop_run_bun_tick(struct us_loop_t *loop, int64_t timeoutMs) { if (loop->num_polls == 0) return; @@ -191,10 +187,6 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, int64_t timeoutMs, void* tickC us_loop_integrate(loop); } - if (tickCallbackContext) { - bun_on_tick_before(tickCallbackContext); - } - /* Emit pre callback */ us_internal_loop_pre(loop); @@ -221,10 +213,6 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, int64_t timeoutMs, void* tickC } #endif - if (tickCallbackContext) { - bun_on_tick_after(tickCallbackContext); - } - /* Iterate ready polls, dispatching them by type */ for (loop->current_ready_poll = 0; loop->current_ready_poll < loop->num_ready_polls; loop->current_ready_poll++) { struct us_poll_t *poll = GET_READY_POLL(loop, loop->current_ready_poll); diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index fed6d579bf..64c145371f 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -400,6 +400,9 @@ int us_raw_root_certs(struct us_cert_string_t**out); unsigned int us_get_remote_address_info(char *buf, struct us_socket_t *s, const char **dest, int *port, int *is_ipv6); int us_socket_get_error(int ssl, struct us_socket_t *s); +void us_socket_ref(struct us_socket_t *s); +void us_socket_unref(struct us_socket_t *s); + #ifdef __cplusplus } #endif diff --git a/packages/bun-usockets/src/socket.c b/packages/bun-usockets/src/socket.c index 52eecb27f9..fb51e63269 100644 --- a/packages/bun-usockets/src/socket.c +++ b/packages/bun-usockets/src/socket.c @@ -387,3 +387,17 @@ unsigned int us_get_remote_address_info(char *buf, struct us_socket_t *s, const return length; } + +void us_socket_ref(struct us_socket_t *s) { +#ifdef LIBUS_USE_LIBUV + uv_ref((uv_handle_t*)s->p.uv_p); +#endif + // do nothing if not using libuv +} + +void us_socket_unref(struct us_socket_t *s) { +#ifdef LIBUS_USE_LIBUV + uv_unref((uv_handle_t*)s->p.uv_p); +#endif + // do nothing if not using libuv +} \ No newline at end of file diff --git a/packages/bun-uws/src/HttpContext.h b/packages/bun-uws/src/HttpContext.h index c6f1bada78..2302740298 100644 --- a/packages/bun-uws/src/HttpContext.h +++ b/packages/bun-uws/src/HttpContext.h @@ -134,6 +134,8 @@ private: /* Handle HTTP data streams */ us_socket_context_on_data(SSL, getSocketContext(), [](us_socket_t *s, char *data, int length) { + // ref the socket to make sure we process it entirely before it is closed + us_socket_ref(s); // total overhead is about 210k down to 180k // ~210k req/sec is the original perf with write in data @@ -293,6 +295,10 @@ private: /* We need to uncork in all cases, except for nullptr (closed socket, or upgraded socket) */ if (returnedSocket != nullptr) { + us_socket_t* returnedSocketPtr = (us_socket_t*) returnedSocket; + /* We don't want open sockets to keep the event loop alive between HTTP requests */ + us_socket_unref(returnedSocketPtr); + /* Timeout on uncork failure */ auto [written, failed] = ((AsyncSocket *) returnedSocket)->uncork(); if (failed) { @@ -312,8 +318,7 @@ private: } } } - - return (us_socket_t *) returnedSocket; + return returnedSocketPtr; } /* If we upgraded, check here (differ between nullptr close and nullptr upgrade) */ diff --git a/packages/bun-uws/src/HttpParser.h b/packages/bun-uws/src/HttpParser.h index f119dcf0d0..75eedd5c7c 100644 --- a/packages/bun-uws/src/HttpParser.h +++ b/packages/bun-uws/src/HttpParser.h @@ -227,16 +227,15 @@ namespace uWS return unsignedIntegerValue; } - /* RFC 9110 16.3.1 Field Name Registry (TLDR; alnum + hyphen is allowed) - * [...] It MUST conform to the field-name syntax defined in Section 5.1, - * and it SHOULD be restricted to just letters, digits, - * and hyphen ('-') characters, with the first character being a letter. */ - static inline bool isFieldNameByte(unsigned char x) + /* RFC 9110 5.6.2. Tokens */ + static inline bool isFieldNameByte(unsigned char c) { - return (x == '-') | - ((x > '/') & (x < ':')) | - ((x > '@') & (x < '[')) | - ((x > 96) & (x < '{')); + return (c > 32) & (c < 127) & (c != '(') & + (c != ')') & (c != ',') & (c != '/') & + (c != ':') & (c != ';') & (c != '<') & + (c != '=') & (c != '>') & (c != '?') & + (c != '@') & (c != '[') & (c != '\\') & + (c != ']') & (c != '{') & (c != '}'); } static inline uint64_t hasLess(uint64_t x, uint64_t n) @@ -263,23 +262,19 @@ namespace uWS hasMore(x, 'z'); } - static inline void *consumeFieldName(char *p) - { - for (; true; p += 8) - { - uint64_t word; - memcpy(&word, p, sizeof(uint64_t)); - if (notFieldNameWord(word)) - { - while (isFieldNameByte(*(unsigned char *)p)) - { - *(p++) |= 0x20; - } - return (void *)p; + static inline void *consumeFieldName(char *p) { + //for (; true; p += 8) { + //uint64_t word; + //memcpy(&word, p, sizeof(uint64_t)); + //if (notFieldNameWord(word)) { + while (isFieldNameByte(*(unsigned char *)p)) { + *(p++) |= 0x20; } - word |= 0x2020202020202020ull; - memcpy(p, &word, sizeof(uint64_t)); - } + return (void *)p; + //} + //word |= 0x2020202020202020ull; + //memcpy(p, &word, sizeof(uint64_t)); + //} } /* Puts method as key, target as value and returns non-null (or nullptr on error). */ diff --git a/scripts/codegen.ps1 b/scripts/codegen.ps1 deleted file mode 100644 index 7c99d980de..0000000000 --- a/scripts/codegen.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$Script1=(Join-Path $PSScriptRoot "./cross-compile-codegen.sh") -$CrossCompileCodegen=(Get-Content $Script1 -Raw) -$CrossCompileCodegen.Replace("`r`n","`n") | Set-Content $Script1 -Force -NoNewline -$Script2=(Join-Path $PSScriptRoot "../src/codegen/create_hash_table") -$CreateHashTable=(Get-Content $Script2 -Raw) -$CreateHashTable.Replace("`r`n","`n") | Set-Content $Script2 -Force -NoNewline - -& 'C:\Program Files\WSL\wsl.exe' ./scripts/cross-compile-codegen.sh win32 x64 "build" - -Set-Content $Script1 -Force -NoNewline -Value $CrossCompileCodegen -Set-Content $Script2 -Force -NoNewline -Value $CreateHashTable - -# copy into build-release as well -Remove-Item -Path "build-release/codegen" -Recurse -Force -ErrorAction SilentlyContinue -Remove-Item -Path "build-release/js" -Recurse -Force -ErrorAction SilentlyContinue -Copy-Item -Path "build/codegen" -Destination "build-release/codegen" -Recurse -Force -Copy-Item -Path "build/js" -Destination "build-release/js" -Recurse -Force diff --git a/scripts/set-webkit-submodule-to-cmake.ps1 b/scripts/set-webkit-submodule-to-cmake.ps1 new file mode 100644 index 0000000000..975f86fcb8 --- /dev/null +++ b/scripts/set-webkit-submodule-to-cmake.ps1 @@ -0,0 +1,22 @@ +# Navigate to the parent directory of the script +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path +Push-Location $scriptPath\.. +try { + # Get the WEBKIT_TAG value from CMakeLists.txt + $WEBKIT_TAG = Select-String -Path 'CMakeLists.txt' -Pattern 'set\(WEBKIT_TAG (.*?)\)' | ForEach-Object { $_.Matches.Groups[1].Value } + if (-not $WEBKIT_TAG) { + Write-Host "Could not find WEBKIT_TAG in CMakeLists.txt" + exit 1 + } + + Write-Host "Setting WebKit submodule to $WEBKIT_TAG" + + # Navigate to the WebKit submodule directory + Set-Location src/bun.js/WebKit + + # Fetch and reset the submodule to the specified tag + git fetch origin "$WEBKIT_TAG" + git reset --hard "$WEBKIT_TAG" +} finally { + Pop-Location +} \ No newline at end of file diff --git a/src/brotli.zig b/src/brotli.zig index c076240a87..c71d45094a 100644 --- a/src/brotli.zig +++ b/src/brotli.zig @@ -143,6 +143,9 @@ pub const BrotliReaderArrayList = struct { }, .needs_more_input => { + if (in_remaining > 0) { + @panic("Brotli wants more data"); + } this.state = .Inflating; if (is_done) { this.state = .Error; diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit index c3712c13dc..9c501b9aa7 160000 --- a/src/bun.js/WebKit +++ b/src/bun.js/WebKit @@ -1 +1 @@ -Subproject commit c3712c13dcdc091cfe4c7cb8f2c1fd16472e6f92 +Subproject commit 9c501b9aa712b7959f80dc99491e8758c151c20e diff --git a/src/bun.js/api/BunObject.zig b/src/bun.js/api/BunObject.zig index 9a011daf00..76700e1cb4 100644 --- a/src/bun.js/api/BunObject.zig +++ b/src/bun.js/api/BunObject.zig @@ -913,6 +913,7 @@ pub fn getStdin( store.ref(); var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; blob.* = JSC.WebCore.Blob.initWithStore(store, globalThis); + blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } @@ -925,6 +926,7 @@ pub fn getStderr( store.ref(); var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; blob.* = JSC.WebCore.Blob.initWithStore(store, globalThis); + blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } @@ -937,6 +939,7 @@ pub fn getStdout( store.ref(); var blob = bun.default_allocator.create(JSC.WebCore.Blob) catch unreachable; blob.* = JSC.WebCore.Blob.initWithStore(store, globalThis); + blob.allocator = bun.default_allocator; return blob.toJS(globalThis); } @@ -2079,6 +2082,7 @@ pub const Crypto = struct { .err => { const error_instance = value.toErrorInstance(globalObject); globalObject.throwValue(error_instance); + return .zero; }, .hash => |h| { return JSC.ZigString.init(h).toValueGC(globalObject); @@ -5274,10 +5278,8 @@ pub const JSZlib = struct { reader.readAll() catch { defer reader.deinit(); - if (reader.errorMessage()) |msg| { - return ZigString.init(msg).toErrorInstance(globalThis); - } - return ZigString.init("Zlib returned an error").toErrorInstance(globalThis); + globalThis.throwValue(ZigString.init(reader.errorMessage() orelse "Zlib returned an error").toErrorInstance(globalThis)); + return .zero; }; reader.list = .{ .items = reader.list.toOwnedSlice(allocator) catch @panic("TODO") }; reader.list.capacity = reader.list.items.len; @@ -5306,10 +5308,8 @@ pub const JSZlib = struct { reader.readAll() catch { defer reader.deinit(); - if (reader.errorMessage()) |msg| { - return ZigString.init(msg).toErrorInstance(globalThis); - } - return ZigString.init("Zlib returned an error").toErrorInstance(globalThis); + globalThis.throwValue(ZigString.init(reader.errorMessage() orelse "Zlib returned an error").toErrorInstance(globalThis)); + return .zero; }; reader.list = .{ .items = reader.list.toOwnedSlice(allocator) catch @panic("TODO") }; reader.list.capacity = reader.list.items.len; @@ -5336,10 +5336,8 @@ pub const JSZlib = struct { reader.readAll() catch { defer reader.deinit(); - if (reader.errorMessage()) |msg| { - return ZigString.init(msg).toErrorInstance(globalThis); - } - return ZigString.init("Zlib returned an error").toErrorInstance(globalThis); + globalThis.throwValue(ZigString.init(reader.errorMessage() orelse "Zlib returned an error").toErrorInstance(globalThis)); + return .zero; }; reader.list = .{ .items = reader.list.toOwnedSlice(allocator) catch @panic("TODO") }; reader.list.capacity = reader.list.items.len; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 9a0ff4a493..e1d6ee06a5 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -100,6 +100,11 @@ #include "JavaScriptCore/InternalFieldTuple.h" #include "wtf/text/StringToIntegerConversion.h" +static WTF::StringView StringView_slice(WTF::StringView sv, unsigned start, unsigned end) +{ + return sv.substring(start, end - start); +} + template static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res) { @@ -4041,6 +4046,7 @@ public: StringView line = stack.substring(start, end - start); offset = end; + // the proper singular spelling is parenthesis auto openingParenthese = line.reverseFind('('); auto closingParenthese = line.reverseFind(')'); @@ -4052,38 +4058,78 @@ public: return false; } - auto firstColon = line.find(':', openingParenthese + 1); + auto lineInner = StringView_slice(line, openingParenthese + 1, closingParenthese); - // if there is no colon, that means we only have a filename and no line number + { + auto marker1 = 0; + auto marker2 = lineInner.find(':', marker1); - // at foo (native) - if (firstColon == WTF::notFound) { - frame.sourceURL = line.substring(openingParenthese + 1, closingParenthese - openingParenthese - 1); - } else { - // at foo (/path/to/file.js: - frame.sourceURL = line.substring(openingParenthese + 1, firstColon - openingParenthese - 1); - } + if (marker2 == WTF::notFound) { + frame.sourceURL = lineInner; + goto done_block; + } - if (firstColon != WTF::notFound) { + auto marker3 = lineInner.find(':', marker2 + 1); + if (marker3 == WTF::notFound) { + // /path/to/file.js: + // /path/to/file.js:1 + // node:child_process + // C:\Users\dave\bun\file.js - auto secondColon = line.find(':', firstColon + 1); - // at foo (/path/to/file.js:1) - if (secondColon == WTF::notFound) { - if (auto lineNumber = WTF::parseIntegerAllowingTrailingJunk(line.substring(firstColon + 1, closingParenthese - firstColon - 1))) { - frame.lineNumber = WTF::OrdinalNumber::fromOneBasedInt(lineNumber.value()); + marker3 = lineInner.length(); + + auto segment1 = StringView_slice(lineInner, marker1, marker2); + auto segment2 = StringView_slice(lineInner, marker2 + 1, marker3); + + if (auto int1 = WTF::parseIntegerAllowingTrailingJunk(segment2)) { + frame.sourceURL = segment1; + frame.lineNumber = WTF::OrdinalNumber::fromOneBasedInt(int1.value()); + } else { + frame.sourceURL = StringView_slice(lineInner, marker1, marker3); + } + goto done_block; + } + + // /path/to/file.js:1: + // /path/to/file.js:1:2 + // node:child_process:1:2 + // C:\Users\dave\bun\file.js: + // C:\Users\dave\bun\file.js:1 + // C:\Users\dave\bun\file.js:1:2 + + while (true) { + auto newcolon = lineInner.find(':', marker3 + 1); + if (newcolon == WTF::notFound) + break; + marker2 = marker3; + marker3 = newcolon; + } + + auto marker4 = lineInner.length(); + + auto segment1 = StringView_slice(lineInner, marker1, marker2); + auto segment2 = StringView_slice(lineInner, marker2 + 1, marker3); + auto segment3 = StringView_slice(lineInner, marker3 + 1, marker4); + + if (auto int1 = WTF::parseIntegerAllowingTrailingJunk(segment2)) { + if (auto int2 = WTF::parseIntegerAllowingTrailingJunk(segment3)) { + frame.sourceURL = segment1; + frame.lineNumber = WTF::OrdinalNumber::fromOneBasedInt(int1.value()); + frame.columnNumber = WTF::OrdinalNumber::fromOneBasedInt(int2.value()); + } else { + frame.sourceURL = segment1; + frame.lineNumber = WTF::OrdinalNumber::fromOneBasedInt(int1.value()); } } else { - // at foo (/path/to/file.js:1:) - if (auto lineNumber = WTF::parseIntegerAllowingTrailingJunk(line.substring(firstColon + 1, secondColon - firstColon - 1))) { - frame.lineNumber = WTF::OrdinalNumber::fromOneBasedInt(lineNumber.value()); - } - - // at foo (/path/to/file.js:1:2) - if (auto columnNumber = WTF::parseIntegerAllowingTrailingJunk(line.substring(secondColon + 1, closingParenthese - secondColon - 1))) { - frame.columnNumber = WTF::OrdinalNumber::fromOneBasedInt(columnNumber.value()); + if (auto int2 = WTF::parseIntegerAllowingTrailingJunk(segment3)) { + frame.sourceURL = StringView_slice(lineInner, marker1, marker3); + frame.lineNumber = WTF::OrdinalNumber::fromOneBasedInt(int2.value()); + } else { + frame.sourceURL = StringView_slice(lineInner, marker1, marker4); } } } + done_block: StringView functionName = line.substring(0, openingParenthese - 1); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 2cde34441c..3878b6522b 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1722,7 +1722,7 @@ pub const VirtualMachine = struct { "../", }; - break :name bun.path.joinAbsStringBuf( + break :name bun.path.joinAbsStringBufZTrailingSlash( jsc_vm.bundler.fs.top_level_dir, &specifier_cache_resolver_buf, &parts, diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 835bfdfa44..9f20a14284 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -86,6 +86,8 @@ pub const Blob = struct { closing, }; + reported_estimated_size: usize = 0, + size: SizeType = 0, offset: SizeType = 0, /// When set, the blob will be freed on finalization callbacks @@ -1424,32 +1426,29 @@ pub const Blob = struct { return blob_; } - fn estimatedByteSize(this: *Blob) usize { + fn calculateEstimatedByteSize(this: *Blob) void { // in-memory size. not the size on disk. - if (this.size != Blob.max_size) { - return this.size; - } - - const store = this.store orelse return 0; - if (store.data == .bytes) { - return store.data.bytes.len; - } - - return 0; - } - - pub fn estimatedSize(this: *Blob) callconv(.C) usize { - var size = this.estimatedByteSize() + @sizeOf(Blob); + var size: usize = @sizeOf(Blob); if (this.store) |store| { size += @sizeOf(Blob.Store); - size += switch (store.data) { - .bytes => store.data.bytes.stored_name.estimatedSize(), - .file => store.data.file.pathlike.estimatedSize(), - }; + switch (store.data) { + .bytes => { + size += store.data.bytes.stored_name.estimatedSize(); + size += if (this.size != Blob.max_size) + this.size + else + store.data.bytes.len; + }, + .file => size += store.data.file.pathlike.estimatedSize(), + } } - return size + (this.content_type.len * @as(usize, @intFromBool(this.content_type_allocated))); + this.reported_estimated_size = size + (this.content_type.len * @intFromBool(this.content_type_allocated)); + } + + pub fn estimatedSize(this: *Blob) callconv(.C) usize { + return this.reported_estimated_size; } comptime { @@ -1526,12 +1525,17 @@ pub const Blob = struct { const path: JSC.Node.PathOrFileDescriptor = brk: { switch (path_or_fd.*) { .path => { - const slice = path_or_fd.path.slice(); + var slice = path_or_fd.path.slice(); if (Environment.isWindows and bun.strings.eqlComptime(slice, "/dev/null")) { - // it is okay to use rodata here, because the '.string' case - // in PathLike.deinit does not free anything. - path_or_fd.* = .{ .path = .{ .string = bun.PathString.init("\\\\.\\NUL") } }; + path_or_fd.deinit(); + path_or_fd.* = .{ + .path = .{ + // this memory is freed with this allocator in `Blob.Store.deinit` + .string = bun.PathString.init(allocator.dupe(u8, "\\\\.\\NUL") catch bun.outOfMemory()), + }, + }; + slice = path_or_fd.path.slice(); } if (vm.standalone_module_graph) |graph| { @@ -3406,6 +3410,8 @@ pub const Blob = struct { }, } + blob.calculateEstimatedByteSize(); + var blob_ = bun.new(Blob, blob); blob_.allocator = allocator; return blob_; @@ -3576,6 +3582,15 @@ pub const Blob = struct { return duped; } + pub fn toJS(this: *Blob, globalObject: *JSC.JSGlobalObject) JSC.JSValue { + if (comptime Environment.allow_assert) { + std.debug.assert(this.allocator != null); + } + + this.calculateEstimatedByteSize(); + return Blob.toJSUnchecked(globalObject, this); + } + pub fn deinit(this: *Blob) void { this.detach(); diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig index 3650c1cd35..5096cb1474 100644 --- a/src/bun.js/webcore/request.zig +++ b/src/bun.js/webcore/request.zig @@ -71,7 +71,7 @@ pub const Request = struct { upgrader: ?*anyopaque = null, // We must report a consistent value for this - reported_estimated_size: ?u63 = null, + reported_estimated_size: usize = 0, const RequestMixin = BodyMixin(@This()); pub usingnamespace JSC.Codegen.JSRequest; @@ -128,10 +128,16 @@ pub const Request = struct { } pub fn estimatedSize(this: *Request) callconv(.C) usize { - return this.reported_estimated_size orelse brk: { - this.reported_estimated_size = @as(u63, @truncate(this.body.value.estimatedSize() + this.sizeOfURL() + @sizeOf(Request))); - break :brk this.reported_estimated_size.?; - }; + return this.reported_estimated_size; + } + + pub fn calculateEstimatedByteSize(this: *Request) void { + this.reported_estimated_size = this.body.value.estimatedSize() + this.sizeOfURL() + @sizeOf(Request); + } + + pub fn toJS(this: *Request, globalObject: *JSGlobalObject) JSValue { + this.calculateEstimatedByteSize(); + return Request.toJSUnchecked(globalObject, this); } pub fn writeFormat(this: *Request, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void { @@ -680,6 +686,8 @@ pub const Request = struct { req.headers.?.put("content-type", req.body.value.Blob.content_type, globalThis); } + req.calculateEstimatedByteSize(); + return req; } pub fn constructor( diff --git a/src/bun.js/webcore/response.classes.ts b/src/bun.js/webcore/response.classes.ts index 5334040676..ed4c5c14e6 100644 --- a/src/bun.js/webcore/response.classes.ts +++ b/src/bun.js/webcore/response.classes.ts @@ -9,6 +9,7 @@ export default [ JSType: "0b11101110", estimatedSize: true, configurable: false, + overridesToJS: true, proto: { text: { fn: "getText" }, json: { fn: "getJSON" }, @@ -68,6 +69,7 @@ export default [ JSType: "0b11101110", configurable: false, estimatedSize: true, + overridesToJS: true, klass: { json: { fn: "constructJSON", @@ -128,6 +130,7 @@ export default [ structuredClone: { transferable: false, tag: 254 }, estimatedSize: true, values: ["stream"], + overridesToJS: true, proto: { text: { fn: "getText" }, json: { fn: "getJSON" }, diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig index 3a2e5697ef..3b64b043fa 100644 --- a/src/bun.js/webcore/response.zig +++ b/src/bun.js/webcore/response.zig @@ -66,7 +66,7 @@ pub const Response = struct { redirected: bool = false, // We must report a consistent value for this - reported_estimated_size: ?u63 = null, + reported_estimated_size: usize = 0, pub const getText = ResponseMixin.getText; pub const getBody = ResponseMixin.getBody; @@ -85,13 +85,19 @@ pub const Response = struct { } pub fn estimatedSize(this: *Response) callconv(.C) usize { - return this.reported_estimated_size orelse brk: { - this.reported_estimated_size = @as( - u63, - @intCast(this.body.value.estimatedSize() + this.url.byteSlice().len + this.init.status_text.byteSlice().len + @sizeOf(Response)), - ); - break :brk this.reported_estimated_size.?; - }; + return this.reported_estimated_size; + } + + pub fn calculateEstimatedByteSize(this: *Response) void { + this.reported_estimated_size = this.body.value.estimatedSize() + + this.url.byteSlice().len + + this.init.status_text.byteSlice().len + + @sizeOf(Response); + } + + pub fn toJS(this: *Response, globalObject: *JSGlobalObject) JSValue { + this.calculateEstimatedByteSize(); + return Response.toJSUnchecked(globalObject, this); } pub fn getBodyValue( @@ -501,6 +507,8 @@ pub const Response = struct { response.init.headers.?.put("content-type", response.body.value.Blob.content_type, globalThis); } + response.calculateEstimatedByteSize(); + return response; } diff --git a/src/bun.zig b/src/bun.zig index cf0674a2b3..8d007f1050 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -68,6 +68,7 @@ pub const FileDescriptor = enum(FileDescriptorInt) { _, pub inline fn int(fd: FileDescriptor) FileDescriptorInt { + // TODO(@paperdave): make this a compile error to call on windows. every usage is incorrect. return @intFromEnum(fd); } diff --git a/src/bundler.zig b/src/bundler.zig index 9dc6112e24..3b07c1c5d0 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -372,19 +372,55 @@ pub const Bundler = struct { this.resolver.allocator = allocator; } - pub inline fn resolveEntryPoint(bundler: *Bundler, entry_point: string) anyerror!_resolver.Result { + fn _resolveEntryPoint(bundler: *Bundler, entry_point: string) !_resolver.Result { return bundler.resolver.resolve(bundler.fs.top_level_dir, entry_point, .entry_point) catch |err| { - const has_dot_slash_form = !strings.hasPrefix(entry_point, "./") and brk: { - return bundler.resolver.resolve(bundler.fs.top_level_dir, try strings.append(bundler.allocator, "./", entry_point), .entry_point) catch break :brk false; - }; - _ = has_dot_slash_form; - - bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} resolving \"{s}\" (entry point)", .{ @errorName(err), entry_point }) catch unreachable; - + if (!std.fs.path.isAbsolute(entry_point) and !strings.hasPrefix(entry_point, "./")) brk: { + return bundler.resolver.resolve( + bundler.fs.top_level_dir, + try strings.append(bundler.allocator, "./", entry_point), + .entry_point, + ) catch + // return the original error + break :brk; + } return err; }; } + pub fn resolveEntryPoint(bundler: *Bundler, entry_point: string) !_resolver.Result { + return _resolveEntryPoint(bundler, entry_point) catch |err| { + var cache_bust_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + + // Bust directory cache and try again + const buster_name = name: { + if (std.fs.path.isAbsolute(entry_point)) { + if (std.fs.path.dirname(entry_point)) |dir| { + break :name strings.withTrailingSlash(dir, entry_point); + } + } + + var parts = [_]string{ + entry_point, + "../", + }; + + break :name bun.path.joinAbsStringBufZTrailingSlash( + bundler.fs.top_level_dir, + &cache_bust_buf, + &parts, + .auto, + ); + }; + + bundler.resolver.bustDirCache(buster_name); + + return _resolveEntryPoint(bundler, entry_point) catch { + bundler.log.addErrorFmt(null, logger.Loc.Empty, bundler.allocator, "{s} resolving \"{s}\" (entry point)", .{ @errorName(err), entry_point }) catch bun.outOfMemory(); + return err; + }; + }; + } + pub fn init( allocator: std.mem.Allocator, log: *logger.Log, diff --git a/src/cache.zig b/src/cache.zig index 1efaf3156a..42e990ccbd 100644 --- a/src/cache.zig +++ b/src/cache.zig @@ -189,7 +189,7 @@ pub const Fs = struct { } if (comptime !Environment.isWindows) // skip on Windows because NTCreateFile will do it. - debug("openat({d}, {s}) = {d}", .{ dirname_fd, path, bun.toFD(file_handle.handle).int() }); + debug("openat({d}, {s}) = {}", .{ dirname_fd, path, bun.toFD(file_handle.handle) }); const will_close = rfs.needToCloseFiles() and _file_handle == null; defer { diff --git a/src/cli/install.sh b/src/cli/install.sh index 4afd50aef6..1bb2eae9d1 100644 --- a/src/cli/install.sh +++ b/src/cli/install.sh @@ -2,8 +2,8 @@ set -euo pipefail if [[ ${OS:-} = Windows_NT ]]; then - echo 'error: Please install bun using Windows Subsystem for Linux' - exit 1 + powershell -c "irm bun.sh/install.ps1|iex" + exit $? fi # Reset diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 3995db0b00..80706331cf 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -43,6 +43,7 @@ export interface ClassDefinition { construct?: boolean; call?: boolean; finalize?: boolean; + overridesToJS?: boolean; klass: Record; proto: Record; values?: string[]; @@ -76,6 +77,7 @@ export function define( klass = {}, proto = {}, values = [], + overridesToJS = false, estimatedSize = false, call = false, construct = false, @@ -86,6 +88,7 @@ export function define( return { ...rest, call, + overridesToJS, construct, estimatedSize, structuredClone, diff --git a/src/codegen/client-js.ts b/src/codegen/client-js.ts index 4dfa6acf6c..d50368a995 100644 --- a/src/codegen/client-js.ts +++ b/src/codegen/client-js.ts @@ -1,3 +1,5 @@ +import { pathToUpperSnakeCase } from "./helpers"; + // This is the implementation for $debug export function createLogClientJS(filepath: string, publicName: string) { return ` @@ -5,16 +7,8 @@ let $debug_log_enabled = ((env) => ( // The rationale for checking all these variables is just so you don't have to exactly remember which one you set. (env.BUN_DEBUG_ALL && env.BUN_DEBUG_ALL !== '0') || (env.BUN_DEBUG_JS && env.BUN_DEBUG_JS !== '0') - || (env.BUN_DEBUG_${filepath - .replace(/^.*?:/, "") - .split(/[-_./]/g) - .join("_") - .toUpperCase()}) - || (env.DEBUG_${filepath - .replace(/^.*?:/, "") - .split(/[-_./]/g) - .join("_") - .toUpperCase()}) + || (env.BUN_DEBUG_${pathToUpperSnakeCase(filepath)}) + || (env.DEBUG_${pathToUpperSnakeCase(filepath)}) ))(Bun.env); let $debug_pid_prefix = Bun.env.SHOW_PID === '1'; let $debug_log = $debug_log_enabled ? (...args) => { diff --git a/src/codegen/create-hash-table.ts b/src/codegen/create-hash-table.ts index 7d646b2f1c..a94dc3d7c2 100644 --- a/src/codegen/create-hash-table.ts +++ b/src/codegen/create-hash-table.ts @@ -18,8 +18,9 @@ const to_remove = new RegExp(`#if\\s+(!OS\\(${os}\\)|OS\\((${other_oses.join("|" const input_preprocessed = to_preprocess.replace(to_remove, ""); +console.log("Generating " + output + " from " + input); const proc = spawn({ - cmd: [create_hash_table, "-"], + cmd: ["perl", create_hash_table, "-"], stdin: "pipe", stdout: "pipe", stderr: "inherit", diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index 8fb0ecd83b..ee7c0136bc 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -565,7 +565,12 @@ JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ${name}::construct(JSC::JSGlobalObj ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr); ${ obj.estimatedSize - ? `vm.heap.reportExtraMemoryAllocated(instance, ${symbolName(obj.name, "estimatedSize")}(instance->wrapped()));` + ? ` + auto size = ${symbolName(typeName, "estimatedSize")}(ptr); +#if ASSERT_ENABLED + ASSERT(size > 0); +#endif + vm.heap.reportExtraMemoryAllocated(instance, size);` : "" } @@ -1225,7 +1230,11 @@ void ${name}::visitChildrenImpl(JSCell* cell, Visitor& visitor) ${ estimatedSize ? `if (auto* ptr = thisObject->wrapped()) { -visitor.reportExtraMemoryVisited(${symbolName(obj.name, "estimatedSize")}(ptr)); + auto size = ${symbolName(typeName, "estimatedSize")}(ptr); +#if ASSERT_ENABLED + ASSERT(size > 0); +#endif +visitor.reportExtraMemoryVisited(size); }` : "" } @@ -1393,7 +1402,12 @@ extern "C" EncodedJSValue ${typeName}__create(Zig::GlobalObject* globalObject, v ${className(typeName)}* instance = ${className(typeName)}::create(vm, globalObject, structure, ptr); ${ obj.estimatedSize - ? `vm.heap.reportExtraMemoryAllocated(instance, ${symbolName(obj.name, "estimatedSize")}(ptr));` + ? ` + auto size = ${symbolName(typeName, "estimatedSize")}(ptr); +#if ASSERT_ENABLED + ASSERT(size > 0); +#endif + vm.heap.reportExtraMemoryAllocated(instance, size);` : "" } return JSValue::encode(instance); @@ -1435,6 +1449,7 @@ function generateZig( construct, finalize, noConstructor = false, + overridesToJS = false, estimatedSize, call = false, values = [], @@ -1703,6 +1718,10 @@ pub const ${className(typeName)} = struct { ` : "" } + + ${ + !overridesToJS + ? ` /// Create a new instance of ${typeName} pub fn toJS(this: *${typeName}, globalObject: *JSC.JSGlobalObject) JSC.JSValue { JSC.markBinding(@src()); @@ -1713,6 +1732,8 @@ pub const ${className(typeName)} = struct { } else { return ${symbolName(typeName, "create")}(globalObject, this); } + }` + : "" } /// Modify the internal ptr to point to a new instance of ${typeName}. @@ -1732,6 +1753,9 @@ pub const ${className(typeName)} = struct { extern fn ${symbolName(typeName, "create")}(globalObject: *JSC.JSGlobalObject, ptr: ?*${typeName}) JSC.JSValue; + /// Create a new instance of ${typeName} without validating it works. + pub const toJSUnchecked = ${symbolName(typeName, "create")}; + extern fn ${typeName}__dangerouslySetPtr(JSC.JSValue, ?*${typeName}) bool; ${renderedCallbacks} diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 4880220b95..46bfb04093 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -980,6 +980,7 @@ await Bun.write(resolve(outDir + "/JSSink.lut.txt"), lutInput()); Bun.spawnSync( [ process.execPath, + "run", join(import.meta.dir, "create-hash-table.ts"), resolve(outDir + "/JSSink.lut.txt"), join(outDir, "JSSink.lut.h"), diff --git a/src/codegen/helpers.ts b/src/codegen/helpers.ts index e1683376e1..f8dd9bba8f 100644 --- a/src/codegen/helpers.ts +++ b/src/codegen/helpers.ts @@ -80,3 +80,11 @@ export function writeIfNotChanged(file: string, contents: string) { fs.writeFileSync(file, contents); } } + +export function pathToUpperSnakeCase(filepath: string) { + return filepath + .replace(/^.*?:/, "") + .split(/[-_./\\]/g) + .join("_") + .toUpperCase(); +} diff --git a/src/codegen/internal-module-registry-scanner.ts b/src/codegen/internal-module-registry-scanner.ts index 362b02e5e1..6efcd1fc18 100644 --- a/src/codegen/internal-module-registry-scanner.ts +++ b/src/codegen/internal-module-registry-scanner.ts @@ -7,6 +7,7 @@ export function createInternalModuleRegistry(basedir: string) { .flatMap(dir => readdirRecursive(path.join(basedir, dir))) .filter(file => file.endsWith(".js") || (file.endsWith(".ts") && !file.endsWith(".d.ts"))) .map(file => file.slice(basedir.length + 1)) + .map(x => x.replaceAll("\\", "/")) .sort(); // Create the Internal Module Registry @@ -66,7 +67,7 @@ export function createInternalModuleRegistry(basedir: string) { resolveSyncOrNull(specifier, path.join(basedir, path.dirname(from))) ?? resolveSyncOrNull(specifier, basedir); if (relativeMatch) { - const found = moduleList.indexOf(path.relative(basedir, relativeMatch)); + const found = moduleList.indexOf(path.relative(basedir, relativeMatch).replaceAll("\\", "/")); if (found === -1) { throw new Error( `Builtin Bundler: "${specifier}" cannot be imported here because it doesn't get a module ID. Only files in "src/js" besides "src/js/builtins" can be used here. Note that the 'node:' or 'bun:' prefix is required here. `, diff --git a/src/fs.zig b/src/fs.zig index 1c338186cf..f4b187be5a 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -750,9 +750,7 @@ pub const FileSystem = struct { }; pub fn needToCloseFiles(rfs: *const RealFS) bool { - // On Windows, we must always close open file handles - // Windows locks files - if (comptime !FeatureFlags.store_file_descriptors) { + if (!FeatureFlags.store_file_descriptors) { return true; } diff --git a/src/http.zig b/src/http.zig index 432c996525..88a7b3c52a 100644 --- a/src/http.zig +++ b/src/http.zig @@ -1307,7 +1307,7 @@ const Decompressor = union(enum) { }, .brotli => |reader| { reader.input = buffer; - reader.total_in = @as(u32, @truncate(buffer.len)); + reader.total_in = 0; const initial = body_out_str.list.items.len; reader.list = body_out_str.list; diff --git a/src/http/mime_type.zig b/src/http/mime_type.zig index 555adaa70f..b24a0fc37b 100644 --- a/src/http/mime_type.zig +++ b/src/http/mime_type.zig @@ -91,7 +91,7 @@ pub const Category = enum { pub const none = MimeType.initComptime("", .none); pub const other = MimeType.initComptime("application/octet-stream", .other); -pub const css = MimeType.initComptime("text/css", .css); +pub const css = MimeType.initComptime("text/css;charset=utf-8", .css); pub const javascript = MimeType.initComptime("text/javascript;charset=utf-8", .javascript); pub const ico = MimeType.initComptime("image/vnd.microsoft.icon", .image); pub const html = MimeType.initComptime("text/html;charset=utf-8", .html); diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index 98a5b0a713..825eb14a85 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -29,8 +29,22 @@ function hideFromStack(...fns) { } } +let warned; +function warnNotImplementedOnce(feature: string, issue?: number) { + if (!warned) { + warned = new Set(); + } + + if (warned.has(feature)) { + return; + } + warned.add(feature); + console.warn(new NotImplementedError(feature, issue)); +} + export default { NotImplementedError, throwNotImplemented, hideFromStack, + warnNotImplementedOnce, }; diff --git a/src/js/node/http.ts b/src/js/node/http.ts index c9f33d9bbe..cee03c9e76 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -5,7 +5,7 @@ const { Duplex, Readable, Writable } = require("node:stream"); const { getHeader, setHeader, assignHeaders: assignHeadersFast } = $lazy("http"); const GlobalPromise = globalThis.Promise; - +let warnNotImplementedOnce; const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; /** * True if val contains an invalid field-vchar @@ -186,7 +186,9 @@ var FakeSocket = class Socket extends Duplex { } } - ref() {} + ref() { + return this; + } get remoteAddress() { return this.address()?.address; @@ -224,10 +226,17 @@ var FakeSocket = class Socket extends Duplex { } setTimeout(timeout, callback) { + if (!warnNotImplementedOnce) { + ({ warnNotImplementedOnce } = require("internal/shared")); + } + + warnNotImplementedOnce("Socket in HTTP setTimeout"); return this; } - unref() {} + unref() { + return this; + } _write(chunk, encoding, callback) {} }; @@ -598,7 +607,14 @@ Server.prototype.listen = function (port, host, backlog, onListen) { return this; }; -Server.prototype.setTimeout = function (msecs, callback) {}; + +Server.prototype.setTimeout = function (msecs, callback) { + if (!warnNotImplementedOnce) { + ({ warnNotImplementedOnce } = require("internal/shared")); + } + warnNotImplementedOnce("Server.setTimeout"); + return this; +}; function assignHeadersSlow(object, req) { const headers = req.headers; @@ -841,7 +857,12 @@ Object.defineProperty(IncomingMessage.prototype, "socket", { }); IncomingMessage.prototype.setTimeout = function (msecs, callback) { - throw new Error("not implemented"); + if (!warnNotImplementedOnce) { + ({ warnNotImplementedOnce } = require("internal/shared")); + } + warnNotImplementedOnce("IncomingMessage.setTimeout"); + + return this; }; function emitErrorNt(msg, err, callback) { @@ -1263,7 +1284,12 @@ ServerResponse.prototype.writeContinue = function (callback) { }; ServerResponse.prototype.setTimeout = function (msecs, callback) { - throw new Error("not implemented"); + if (!warnNotImplementedOnce) { + ({ warnNotImplementedOnce } = require("internal/shared")); + } + + warnNotImplementedOnce("ServerResponse.setTimeout"); + return this; }; ServerResponse.prototype.appendHeader = function (name, value) { diff --git a/src/js/node/worker_threads.ts b/src/js/node/worker_threads.ts index c4cf1b3fe8..bbafed0eba 100644 --- a/src/js/node/worker_threads.ts +++ b/src/js/node/worker_threads.ts @@ -4,7 +4,7 @@ declare const self: typeof globalThis; type WebWorker = InstanceType; const EventEmitter = require("node:events"); -const { throwNotImplemented } = require("../internal/shared"); +const { throwNotImplemented, warnNotImplementedOnce } = require("../internal/shared"); const { MessageChannel, BroadcastChannel, Worker: WebWorker } = globalThis; const SHARE_ENV = Symbol("nodejs.worker_threads.SHARE_ENV"); @@ -14,14 +14,6 @@ let [_workerData, _threadId, _receiveMessageOnPort] = $lazy("worker_threads"); type NodeWorkerOptions = import("node:worker_threads").WorkerOptions; -const emittedWarnings = new Set(); -function emitWarning(type, message) { - if (emittedWarnings.has(type)) return; - emittedWarnings.add(type); - // process.emitWarning(message); // our printing is bad - console.warn("[bun] Warning:", message); -} - function injectFakeEmitter(Class) { function messageEventHandler(event: MessageEvent) { return event.data; @@ -216,7 +208,7 @@ class Worker extends EventEmitter { super(); for (const key of unsupportedOptions) { if (key in options && options[key] != null) { - emitWarning("option." + key, `worker_threads.Worker option "${key}" is not implemented.`); + warnNotImplementedOnce(`worker_threads.Worker option "${key}"`); } } this.#worker = new WebWorker(filename, options); @@ -257,7 +249,7 @@ class Worker extends EventEmitter { get performance() { return (this.#performance ??= { eventLoopUtilization() { - emitWarning("performance", "worker_threads.Worker.performance is not implemented."); + warnNotImplementedOnce("worker_threads.Worker.performance"); return { idle: 0, active: 0, diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index d13122c254..3f25db0e70 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -2127,10 +2127,12 @@ pub const PosixToWinNormalizer = struct { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { std.debug.assert(isSepAny(root[0])); - const source_root = windowsFilesystemRoot(source_dir); - @memcpy(buf[0..source_root.len], source_root); - @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); - return buf[0 .. source_root.len + maybe_posix_path.len - 1]; + if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { + const source_root = windowsFilesystemRoot(source_dir); + @memcpy(buf[0..source_root.len], source_root); + @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); + return buf[0 .. source_root.len + maybe_posix_path.len - 1]; + } } } return maybe_posix_path; @@ -2146,13 +2148,14 @@ pub const PosixToWinNormalizer = struct { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { std.debug.assert(isSepAny(root[0])); - // note: bun.getcwd will return forward slashes, not what we want. - const cwd = try std.os.getcwd(buf); - std.debug.assert(cwd.ptr == buf.ptr); - const source_root = windowsFilesystemRoot(cwd); - std.debug.assert(source_root.ptr == source_root.ptr); - @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); - return buf[0 .. source_root.len + maybe_posix_path.len - 1]; + if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { + const cwd = try std.os.getcwd(buf); + std.debug.assert(cwd.ptr == buf.ptr); + const source_root = windowsFilesystemRoot(cwd); + std.debug.assert(source_root.ptr == source_root.ptr); + @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); + return buf[0 .. source_root.len + maybe_posix_path.len - 1]; + } } } @@ -2169,14 +2172,15 @@ pub const PosixToWinNormalizer = struct { const root = windowsFilesystemRoot(maybe_posix_path); if (root.len == 1) { std.debug.assert(isSepAny(root[0])); - // note: bun.getcwd will return forward slashes, not what we want. - const cwd = try std.os.getcwd(buf); - std.debug.assert(cwd.ptr == buf.ptr); - const source_root = windowsFilesystemRoot(cwd); - std.debug.assert(source_root.ptr == source_root.ptr); - @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); - buf[source_root.len + maybe_posix_path.len - 1] = 0; - return buf[0 .. source_root.len + maybe_posix_path.len - 1 :0]; + if (bun.strings.isWindowsAbsolutePathMissingDriveLetter(u8, maybe_posix_path)) { + const cwd = try std.os.getcwd(buf); + std.debug.assert(cwd.ptr == buf.ptr); + const source_root = windowsFilesystemRoot(cwd); + std.debug.assert(source_root.ptr == source_root.ptr); + @memcpy(buf[source_root.len..][0 .. maybe_posix_path.len - 1], maybe_posix_path[1..]); + buf[source_root.len + maybe_posix_path.len - 1] = 0; + return buf[0 .. source_root.len + maybe_posix_path.len - 1 :0]; + } } } diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index caeaf66101..b31b5f75c9 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -1584,6 +1584,7 @@ pub const Resolver = struct { pub fn bustDirCache(r: *ThisResolver, path: string) void { dev("Bust {s}", .{path}); + std.debug.assert(path[path.len - 1] == std.fs.path.sep); r.fs.fs.bustEntriesCache(path); r.dir_cache.remove(path); } @@ -2876,8 +2877,8 @@ pub const Resolver = struct { // because we want the output to always be deterministic if (strings.startsWith(path, prefix) and strings.endsWith(path, suffix) and - (prefix.len >= longest_match_prefix_length and - suffix.len > longest_match_suffix_length)) + (prefix.len > longest_match_prefix_length or + (prefix.len == longest_match_prefix_length and suffix.len > longest_match_suffix_length))) { longest_match_prefix_length = @as(i32, @intCast(prefix.len)); longest_match_suffix_length = @as(i32, @intCast(suffix.len)); @@ -3456,7 +3457,6 @@ pub const Resolver = struct { // Is this a file? if (r.loadAsFile(path, extension_order)) |file| { - // Determine the package folder by looking at the last node_modules/ folder in the path if (strings.lastIndexOf(file.path, "node_modules" ++ std.fs.path.sep_str)) |last_node_modules_folder| { const node_modules_folder_offset = last_node_modules_folder + ("node_modules" ++ std.fs.path.sep_str).len; @@ -3493,12 +3493,9 @@ pub const Resolver = struct { debug.addNoteFmt("Attempting to load \"{s}\" as a directory", .{path}); debug.increaseIndent(); } - - defer { - if (r.debug_logs) |*debug| { - debug.decreaseIndent(); - } - } + defer if (r.debug_logs) |*debug| { + debug.decreaseIndent(); + }; const dir_info = (r.dirInfoCached(path) catch |err| { if (comptime Environment.isDebug) Output.prettyErrorln("err: {s} reading {s}", .{ @errorName(err), path }); diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 740f6e5cbd..ea45ae857b 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1683,22 +1683,41 @@ pub fn utf16Codepoint(comptime Type: type, input: Type) UTF16Replacement { } } -/// '/hello' -> true -/// '\hello' -> true -/// 'C:/hello' -> false -/// '\??\C:\hello' -> false -fn windowsPathIsPosixAbsolute(comptime T: type, chars: []const T) bool { - if (chars.len == 0) return false; - if (!(chars[0] == '/' or chars[0] == '\\')) return false; +/// Checks if a path is missing a windows drive letter. Not a perfect check, +/// but it is good enough for most cases. For windows APIs, this is used for +/// an assertion, and PosixToWinNormalizer can help make an absolute path +/// contain a drive letter. +pub fn isWindowsAbsolutePathMissingDriveLetter(comptime T: type, chars: []const T) bool { + std.debug.assert(bun.path.Platform.windows.isAbsoluteT(T, chars)); + std.debug.assert(chars.len > 0); + + // 'C:\hello' -> false + if (!(chars[0] == '/' or chars[0] == '\\')) { + std.debug.assert(chars.len > 2); + std.debug.assert(chars[1] == ':'); + return false; + } + + // '\\hello' -> false (probably a UNC path) if (chars.len > 1 and (chars[1] == '/' or chars[1] == '\\')) return false; - if (chars.len > 2 and - chars[2] == ':') return false; - if (chars.len > 4 and - chars[1] == '?' and - chars[2] == '?' and - (chars[3] == '/' or chars[3] == '\\')) - return windowsPathIsPosixAbsolute(T, chars[4..]); + + if (chars.len > 4) { + // '\??\hello' -> false (has the NT object prefix) + if (chars[1] == '?' and + chars[2] == '?' and + (chars[3] == '/' or chars[3] == '\\')) + return false; + // '\\?\hello' -> false (has the other NT object prefix) + // '\\.\hello' -> false (has the NT device prefix) + if ((chars[1] == '/' or chars[1] == '\\') and + (chars[2] == '?' or chars[2] == '.') and + (chars[3] == '/' or chars[3] == '\\')) + return false; + } + + // oh no, '/hello/world' + // where is the drive letter! return true; } @@ -1801,7 +1820,9 @@ pub fn toWDirPath(wbuf: []u16, utf8: []const u8) [:0]const u16 { pub fn assertIsValidWindowsPath(comptime T: type, path: []const T) void { if (Environment.allow_assert and Environment.isWindows) { - if (windowsPathIsPosixAbsolute(T, path)) { + if (bun.path.Platform.windows.isAbsoluteT(T, path) and + isWindowsAbsolutePathMissingDriveLetter(T, path)) + { std.debug.panic("Do not pass posix paths to windows APIs, was given '{s}' (missing a root like 'C:\\', see PosixToWinNormalizer for why this is an assertion)", .{ if (T == u8) path else std.unicode.fmtUtf16le(path), }); diff --git a/src/string_types.zig b/src/string_types.zig index c9bc56abde..456c7f186e 100644 --- a/src/string_types.zig +++ b/src/string_types.zig @@ -45,6 +45,10 @@ pub const PathString = packed struct { return this.len == 0; } + pub fn format(self: PathString, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.writeAll(self.slice()); + } + pub const empty = @This(){ .ptr = 0, .len = 0 }; comptime { if (!bun.Environment.isWasm) { diff --git a/src/sys.zig b/src/sys.zig index 36853c74de..f1c316c26d 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -600,7 +600,18 @@ pub fn openDirAtWindowsNtPath( ); if (comptime Environment.allow_assert) { - log("NtCreateFile({d}, {s}, iterable = {}) = {s} (dir) = {d}", .{ dirFd, bun.fmt.fmtUTF16(path), iterable, @tagName(rc), @intFromPtr(fd) }); + if (rc == .INVALID_PARAMETER) { + // Double check what flags you are passing to this + // + // - access_mask probably needs w.SYNCHRONIZE, + // - options probably needs w.FILE_SYNCHRONOUS_IO_NONALERT + // - disposition probably needs w.FILE_OPEN + bun.Output.debugWarn("NtCreateFile({}, {}) = {s} (dir) = {d}\nYou are calling this function with the wrong flags!!!", .{ dirFd, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(fd) }); + } else if (rc == .OBJECT_PATH_SYNTAX_BAD or rc == .OBJECT_NAME_INVALID) { + bun.Output.debugWarn("NtCreateFile({}, {}) = {s} (dir) = {d}\nYou are calling this function without normalizing the path correctly!!!", .{ dirFd, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(fd) }); + } else { + log("NtCreateFile({}, {}) = {s} (dir) = {d}", .{ dirFd, bun.fmt.fmtUTF16(path), @tagName(rc), @intFromPtr(fd) }); + } } switch (windows.Win32Error.fromNTStatus(rc)) { diff --git a/test/js/bun/resolve/bar/larger-index.js b/test/js/bun/resolve/bar/larger-index.js new file mode 100644 index 0000000000..5837bb3bbb --- /dev/null +++ b/test/js/bun/resolve/bar/larger-index.js @@ -0,0 +1,2 @@ +// this file is used in resolve.test.js +export default {}; diff --git a/test/js/bun/resolve/resolve-test.js b/test/js/bun/resolve/resolve-test.js index 242834b6fe..38198ef1d5 100644 --- a/test/js/bun/resolve/resolve-test.js +++ b/test/js/bun/resolve/resolve-test.js @@ -71,6 +71,7 @@ it("import.meta.resolve", async () => { expect(await import.meta.resolve("foo/bar")).toBe(join(import.meta.path, "../baz.js")); expect(await import.meta.resolve("@faasjs/baz")).toBe(join(import.meta.path, "../baz.js")); expect(await import.meta.resolve("@faasjs/bar")).toBe(join(import.meta.path, "../bar/src/index.js")); + expect(await import.meta.resolve("@faasjs/larger/bar")).toBe(join(import.meta.path, "../bar/larger-index.js")); // works with package.json "exports" expect(await import.meta.resolve("package-json-exports/baz")).toBe( diff --git a/test/js/bun/util/file-type.test.ts b/test/js/bun/util/file-type.test.ts index b411710a87..d4d926b0c2 100644 --- a/test/js/bun/util/file-type.test.ts +++ b/test/js/bun/util/file-type.test.ts @@ -11,4 +11,9 @@ describe("util file tests", () => { }); expect(custom_type.type).toBe("custom/mimetype"); }); + + test("mime-type is text/css;charset=utf-8", () => { + const file = Bun.file("test.css"); + expect(file.type).toBe("text/css;charset=utf-8"); + }); }); diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index 5826b2aa34..2dff2d4ad9 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -2729,3 +2729,30 @@ it("fs.ReadStream allows functions", () => { // @ts-expect-error expect(() => new fs.ReadStream(".", {})).not.toThrow(); }); + +describe.if(isWindows)("windows path handling", () => { + const file = import.meta.path.slice(3); + const drive = import.meta.path[0]; + const filenames = [ + `${drive}:\\${file}`, + `\\\\127.0.0.1\\${drive}$\\${file}`, + `\\\\LOCALHOST\\${drive}$\\${file}`, + `\\\\.\\${drive}:\\${file}`, + `\\\\?\\${drive}:\\${file}`, + `\\\\.\\UNC\\LOCALHOST\\${drive}$\\${file}`, + `\\\\?\\UNC\\LOCALHOST\\${drive}$\\${file}`, + `\\\\127.0.0.1\\${drive}$\\${file}`, + ]; + + for (const filename of filenames) { + test(`Can read '${filename}' with node:fs`, async () => { + const stats = await fs.promises.stat(filename); + expect(stats.size).toBeGreaterThan(0); + }); + + test(`Can read '${filename}' with Bun.file`, async () => { + const stats = await Bun.file(filename).text(); + expect(stats.length).toBeGreaterThan(0); + }); + } +}); diff --git a/test/js/node/zlib/zlib.test.js b/test/js/node/zlib/zlib.test.js index b33894a3e7..384be020e2 100644 --- a/test/js/node/zlib/zlib.test.js +++ b/test/js/node/zlib/zlib.test.js @@ -15,6 +15,16 @@ describe("zlib", () => { const decompressed = gunzipSync(compressed); expect(decompressed.join("")).toBe(data.join("")); }); + + it("should throw on invalid raw deflate data", () => { + const data = new TextEncoder().encode("Hello World!".repeat(1)); + expect(() => inflateSync(data)).toThrow(new Error("invalid stored block lengths")); + }); + + it("should throw on invalid gzip data", () => { + const data = new TextEncoder().encode("Hello World!".repeat(1)); + expect(() => gunzipSync(data)).toThrow(new Error("incorrect header check")); + }); }); import * as zlib from "node:zlib"; diff --git a/test/tsconfig.json b/test/tsconfig.json index 09eecec640..248cb67477 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -26,7 +26,8 @@ "node-harness": ["js/node/harness.ts"], "deno:harness": ["js/deno/harness.ts"], "foo/bar": ["js/bun/resolve/baz.js"], - "@faasjs/*": ["js/bun/resolve/*.js", "js/bun/resolve/*/src/index.js"] + "@faasjs/*": ["js/bun/resolve/*.js", "js/bun/resolve/*/src/index.js"], + "@faasjs/larger/*": ["js/bun/resolve/*/larger-index.js"] } },