From 11f1403e0cd60bf226353b72375a1da8344802dc Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 12 Jul 2026 12:51:32 +0200 Subject: [PATCH] modernize node-re2 for Node 26 --- .dockerignore | 6 + .gitignore | 1 + Dockerfile | 26 + README.md | 32 ++ THIRD_PARTY.md | 6 + binding.cc | 461 +++++++++++++---- binding.gyp | 55 +- binding.js | 4 +- build.sh | 39 ++ index.d.ts | 12 +- index.js | 42 +- package-lock.json | 954 +++++++++++++++++++++++++++++++++++ package.json | 76 ++- release.sh | 73 +++ scripts/verify-prebuilds.mjs | 32 ++ test.js | 171 ++++++- tsconfig.json | 12 + type-tests/index.ts | 26 + vendor/abseil-cpp | 2 +- vendor/re2 | 2 +- 20 files changed, 1882 insertions(+), 150 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 THIRD_PARTY.md create mode 100755 build.sh create mode 100644 package-lock.json create mode 100755 release.sh create mode 100644 scripts/verify-prebuilds.mjs create mode 100644 tsconfig.json create mode 100644 type-tests/index.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd102b2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.github +build +node_modules +prebuilds +npm-debug.log diff --git a/.gitignore b/.gitignore index 68d0263..b688ec7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ yarn.lock .vscode build +prebuilds/ .claude diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4848641 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM node:26.5.0-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + python3 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /node-re2 + +COPY package*.json ./ +RUN npm ci --ignore-scripts + +COPY . . + +ARG JOBS=8 +RUN JOBS=$JOBS npx prebuildify \ + -t "$(node -p process.versions.node)" \ + --napi \ + --strip \ + --tag-libc \ + --arch x64 + +# PREBUILDS_ONLY makes node-gyp-build ignore build/Release, proving that the +# binary which will be embedded in the npm tarball is independently loadable. +RUN npm run test:prebuild diff --git a/README.md b/README.md new file mode 100644 index 0000000..4803cb3 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# @nxtedition/re2 + +Native [RE2](https://github.com/google/re2) partial matching for Node.js 26 and later. It exposes a small API for matching one expression or many expressions at once without backtracking. + +```js +import { RE2, RE2Set } from '@nxtedition/re2' + +const expression = new RE2('foo') +expression.test(Buffer.from('before foo after')) // true + +const expressions = new RE2Set(['foo', 'bar']) +expressions.test(Buffer.from('bar')) // [1] +``` + +Patterns may be strings, Buffers, TypedArrays, or DataViews. Input must be a Buffer, TypedArray, or DataView. SharedArrayBuffer-backed views are supported. Both APIs operate on bytes; optional `byteOffset` and `byteLength` values select the input range. Negative values clamp to zero, values past the view clamp to its bounds, and fractional values are truncated. + +Invalid RE2 syntax throws during construction. `RE2Set#test()` returns every matching pattern index, or `[]` when nothing matches; index order is unspecified. + +## Prebuilds + +Release tarballs include Node-API prebuilds for `darwin-arm64` and glibc `linux-x64`. The Linux binary is built in the Node 26 Bookworm image for compatibility with both Bookworm and newer glibc systems, and is libc-tagged so it is never selected on musl. Other platforms fall back to a source build when install scripts are enabled. + +`./build.sh` builds and tests the Linux prebuild in Docker. On an arm64 Mac, `./release.sh` builds both supported platforms, tests the current platform with source-build fallback disabled, verifies that both binaries are present in the npm tarball, and then prompts for the version bump before publishing. + +## Development + +```sh +npm install +npm test +``` + +`npm test` always rebuilds the native addon before running the JavaScript and TypeScript contract tests. diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..b5b6f8d --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,6 @@ +# Third-party sources + +The native addon vendors these dependencies as Git submodules: + +- RE2 `2025-11-05` (`927f5d53caf8111721e734cf24724686bb745f55`), licensed under the BSD 3-Clause License in `vendor/re2/LICENSE`. +- Abseil LTS `20250814.2` (`0cf0a5c9d12cc3783363ab20f11613e69fd04c9a`), licensed under the Apache License 2.0 in `vendor/abseil-cpp/LICENSE`. diff --git a/binding.cc b/binding.cc index be74ede..ac9bd8d 100644 --- a/binding.cc +++ b/binding.cc @@ -1,159 +1,440 @@ -#define NAPI_VERSION 8 +#define NAPI_VERSION 10 -#include -#include #include #include #include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include + +namespace { + +struct ByteView { + const char* data; + size_t size; +}; + +bool Check(napi_env env, napi_status status, const char* operation) { + if (status == napi_ok) { + return true; + } + if (status == napi_pending_exception) { + return false; + } + + const napi_extended_error_info* info = nullptr; + const char* message = operation; + if (napi_get_last_error_info(env, &info) == napi_ok && info != nullptr && + info->error_message != nullptr) { + message = info->error_message; + } + napi_throw_error(env, nullptr, message); + return false; +} + +template +bool GetArguments(napi_env env, napi_callback_info info, + std::array* arguments) { + size_t count = N; + if (!Check(env, + napi_get_cb_info(env, info, &count, arguments->data(), nullptr, + nullptr), + "Failed to read arguments")) { + return false; + } + if (count < N) { + napi_throw_type_error(env, nullptr, "Not enough arguments"); + return false; + } + return true; +} + +bool AssignByteView(napi_env env, void* data, size_t size, ByteView* view) { + if (size != 0 && data == nullptr) { + napi_throw_error(env, nullptr, "Binary input has no data"); + return false; + } + *view = {size == 0 ? "" : static_cast(data), size}; + return true; +} + +size_t TypedArrayElementSize(napi_typedarray_type type) { + switch (type) { + case napi_int8_array: + case napi_uint8_array: + case napi_uint8_clamped_array: + return 1; + case napi_int16_array: + case napi_uint16_array: +#ifdef NODE_API_HAS_FLOAT16_ARRAY + case napi_float16_array: +#endif + return 2; + case napi_int32_array: + case napi_uint32_array: + case napi_float32_array: + return 4; + case napi_float64_array: + case napi_bigint64_array: + case napi_biguint64_array: + return 8; + } + return 0; +} + +bool GetByteView(napi_env env, napi_value value, ByteView* view) { + bool is_buffer = false; + if (!Check(env, napi_is_buffer(env, value, &is_buffer), + "Failed to inspect binary input")) { + return false; + } + if (is_buffer) { + void* data = nullptr; + size_t size = 0; + if (!Check(env, napi_get_buffer_info(env, value, &data, &size), + "Failed to read Buffer")) { + return false; + } + return AssignByteView(env, data, size, view); + } + + bool is_typed_array = false; + if (!Check(env, napi_is_typedarray(env, value, &is_typed_array), + "Failed to inspect binary input")) { + return false; + } + if (is_typed_array) { + napi_typedarray_type type; + size_t length = 0; + void* data = nullptr; + napi_value array_buffer; + size_t byte_offset = 0; + if (!Check(env, + napi_get_typedarray_info(env, value, &type, &length, &data, + &array_buffer, &byte_offset), + "Failed to read TypedArray")) { + return false; + } + (void)array_buffer; + (void)byte_offset; + const size_t element_size = TypedArrayElementSize(type); + if (element_size == 0 || + length > std::numeric_limits::max() / element_size) { + napi_throw_range_error(env, nullptr, "TypedArray is too large"); + return false; + } + const size_t size = length * element_size; + return AssignByteView(env, data, size, view); + } + + bool is_data_view = false; + if (!Check(env, napi_is_dataview(env, value, &is_data_view), + "Failed to inspect binary input")) { + return false; + } + if (is_data_view) { + size_t size = 0; + void* data = nullptr; + napi_value array_buffer; + size_t byte_offset = 0; + if (!Check(env, + napi_get_dataview_info(env, value, &size, &data, &array_buffer, + &byte_offset), + "Failed to read DataView")) { + return false; + } + (void)array_buffer; + (void)byte_offset; + return AssignByteView(env, data, size, view); + } + + napi_throw_type_error(env, nullptr, + "Expected a Buffer, TypedArray, or DataView"); + return false; +} + +bool GetClampedIndex(napi_env env, napi_value value, size_t maximum, + size_t* result) { + double number = 0; + const napi_status status = napi_get_value_double(env, value, &number); + if (status != napi_ok) { + if (status == napi_pending_exception) { + return false; + } + napi_throw_type_error(env, nullptr, "Byte ranges must be numbers"); + return false; + } + + if (std::isnan(number)) { + *result = 0; + } else if (number <= 0) { + *result = 0; + } else if (number >= static_cast(maximum)) { + *result = maximum; + } else { + *result = static_cast(std::trunc(number)); + } + return true; +} + +bool GetText(napi_env env, napi_value input, napi_value offset_value, + napi_value length_value, std::string_view* text) { + ByteView view; + if (!GetByteView(env, input, &view)) { + return false; + } + + size_t offset = 0; + if (!GetClampedIndex(env, offset_value, view.size, &offset)) { + return false; + } + size_t length = 0; + if (!GetClampedIndex(env, length_value, view.size - offset, &length)) { + return false; + } + + *text = std::string_view(view.data + offset, length); + return true; +} + +template +void Finalize(napi_env, void* data, void*) { + delete static_cast(data); +} template -static void Finalize(napi_env env, void* data, void* hint) { - delete reinterpret_cast(data); +bool CreateExternal(napi_env env, std::unique_ptr value, + napi_value* result) { + if (!Check(env, + napi_create_external(env, value.get(), Finalize, nullptr, + result), + "Failed to create native context")) { + return false; + } + value.release(); + return true; } -NAPI_METHOD(regex_init) { - NAPI_ARGV(1); +napi_value RegexInit(napi_env env, napi_callback_info info) { + std::array arguments; + if (!GetArguments(env, info, &arguments)) { + return nullptr; + } try { - std::string_view pattern; - { - char* buf = nullptr; - size_t size = 0; - NAPI_STATUS_THROWS(napi_get_buffer_info(env, argv[0], reinterpret_cast(&buf), &size)); - pattern = std::string_view(buf, size); + ByteView pattern; + if (!GetByteView(env, arguments[0], &pattern)) { + return nullptr; } - auto regex = std::make_unique(pattern); + re2::RE2::Options options; + options.set_log_errors(false); + auto regex = std::make_unique( + std::string_view(pattern.data, pattern.size), options); + if (!regex->ok()) { + napi_throw_error(env, nullptr, regex->error().c_str()); + return nullptr; + } napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, regex.get(), Finalize, nullptr, &result)); - regex.release(); - - return result; - } catch (const std::exception& e) { - napi_throw_error(env, nullptr, e.what()); + return CreateExternal(env, std::move(regex), &result) ? result : nullptr; + } catch (const std::exception& error) { + napi_throw_error(env, nullptr, error.what()); return nullptr; } } -NAPI_METHOD(regex_test) { - NAPI_ARGV(4); +napi_value RegexTest(napi_env env, napi_callback_info info) { + std::array arguments; + if (!GetArguments(env, info, &arguments)) { + return nullptr; + } try { - re2::RE2* regex; - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast(®ex))); + re2::RE2* regex = nullptr; + if (!Check(env, + napi_get_value_external(env, arguments[0], + reinterpret_cast(®ex)), + "Failed to read regex context")) { + return nullptr; + } std::string_view text; - { - char* buf = nullptr; - size_t size = 0; - NAPI_STATUS_THROWS(napi_get_buffer_info(env, argv[1], reinterpret_cast(&buf), &size)); - - NAPI_INT32(offset, argv[2]); - offset = std::max(0, std::min(offset, size)); - - NAPI_INT32(length, argv[3]); - length = std::max(0, std::min(length, size - offset)); - - text = std::string_view(buf + offset, length); + if (!GetText(env, arguments[1], arguments[2], arguments[3], &text)) { + return nullptr; } napi_value result; - NAPI_STATUS_THROWS(napi_get_boolean(env, re2::RE2::PartialMatch(text, *regex), &result)); + if (!Check(env, + napi_get_boolean(env, re2::RE2::PartialMatch(text, *regex), + &result), + "Failed to create match result")) { + return nullptr; + } return result; - } catch (const std::exception& e) { - napi_throw_error(env, nullptr, e.what()); + } catch (const std::exception& error) { + napi_throw_error(env, nullptr, error.what()); return nullptr; } } -NAPI_METHOD(set_init) { - NAPI_ARGV(1); +napi_value SetInit(napi_env env, napi_callback_info info) { + std::array arguments; + if (!GetArguments(env, info, &arguments)) { + return nullptr; + } try { - // TODO (fix): allow options and anchor to be passed in. - RE2::Options options; - RE2::Anchor anchor = RE2::Anchor::UNANCHORED; + bool is_array = false; + if (!Check(env, napi_is_array(env, arguments[0], &is_array), + "Failed to inspect patterns")) { + return nullptr; + } + if (!is_array) { + napi_throw_type_error(env, nullptr, "patterns must be an array"); + return nullptr; + } + + uint32_t pattern_count = 0; + if (!Check(env, + napi_get_array_length(env, arguments[0], &pattern_count), + "Failed to read patterns")) { + return nullptr; + } + if (pattern_count > static_cast(std::numeric_limits::max())) { + napi_throw_range_error(env, nullptr, "Too many patterns"); + return nullptr; + } - auto set = std::make_unique(options, anchor); + re2::RE2::Options options; + options.set_log_errors(false); + auto set = std::make_unique(options, re2::RE2::UNANCHORED); - auto patterns = argv[0]; - NAPI_FOR_EACH(patterns, pattern) { - NAPI_BUFFER(buf, pattern); + for (uint32_t index = 0; index < pattern_count; ++index) { + napi_value pattern_value; + if (!Check(env, + napi_get_element(env, arguments[0], index, &pattern_value), + "Failed to read pattern")) { + return nullptr; + } + ByteView pattern; + if (!GetByteView(env, pattern_value, &pattern)) { + return nullptr; + } std::string error; - auto idx = set->Add(std::string_view(buf, buf_len), &error) ; - if (idx < 0) { + const int pattern_index = set->Add( + std::string_view(pattern.data, pattern.size), &error); + if (pattern_index < 0) { napi_throw_error(env, nullptr, error.c_str()); return nullptr; } - - // TODO (fix): identify pattern with idx and don't - // assume it's the same as the index in the array. - assert(idx == i); + if (pattern_index != static_cast(index)) { + napi_throw_error(env, nullptr, "Unexpected RE2Set pattern index"); + return nullptr; + } } if (!set->Compile()) { - napi_throw_error(env, nullptr, "Failed to compile set"); + napi_throw_error(env, nullptr, "Failed to compile RE2Set"); return nullptr; } napi_value result; - NAPI_STATUS_THROWS(napi_create_external(env, set.get(), Finalize, nullptr, &result)); - set.release(); - - return result; - } catch(const std::exception& e) { - napi_throw_error(env, nullptr, e.what()); + return CreateExternal(env, std::move(set), &result) ? result : nullptr; + } catch (const std::exception& error) { + napi_throw_error(env, nullptr, error.what()); return nullptr; } } -NAPI_METHOD(set_test) { - NAPI_ARGV(4); +napi_value SetTest(napi_env env, napi_callback_info info) { + std::array arguments; + if (!GetArguments(env, info, &arguments)) { + return nullptr; + } try { - re2::RE2::Set* set; - NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], reinterpret_cast(&set))); + re2::RE2::Set* set = nullptr; + if (!Check(env, + napi_get_value_external(env, arguments[0], + reinterpret_cast(&set)), + "Failed to read set context")) { + return nullptr; + } std::string_view text; - { - NAPI_BUFFER(buf, argv[1]); - - NAPI_INT32(offset, argv[2]); - offset = std::max(0, std::min(offset, buf_len)); - - NAPI_INT32(length, argv[3]); - length = std::max(0, std::min(length, buf_len - offset)); - - text = std::string_view(buf + offset, length); + if (!GetText(env, arguments[1], arguments[2], arguments[3], &text)) { + return nullptr; } - napi_value result; - std::vector indices; - set->Match(text, &indices); + re2::RE2::Set::ErrorInfo error_info{re2::RE2::Set::kNoError}; + const bool matched = set->Match(text, &indices, &error_info); + if (!matched && error_info.kind != re2::RE2::Set::kNoError) { + const char* message = "RE2Set matching failed"; + if (error_info.kind == re2::RE2::Set::kOutOfMemory) { + message = "RE2Set matching failed: DFA out of memory"; + } else if (error_info.kind == re2::RE2::Set::kNotCompiled) { + message = "RE2Set matching failed: set is not compiled"; + } else if (error_info.kind == re2::RE2::Set::kInconsistent) { + message = "RE2Set matching failed: inconsistent result"; + } + napi_throw_error(env, nullptr, message); + return nullptr; + } - NAPI_STATUS_THROWS(napi_create_array_with_length(env, indices.size(), &result)); - for (size_t n = 0; n < indices.size(); n++) { + napi_value result; + if (!Check(env, + napi_create_array_with_length(env, indices.size(), &result), + "Failed to create set result")) { + return nullptr; + } + for (size_t index = 0; index < indices.size(); ++index) { napi_value element; - NAPI_STATUS_THROWS(napi_create_int32(env, indices[n], &element)); - NAPI_STATUS_THROWS(napi_set_element(env, result, n, element)); + if (!Check(env, napi_create_int32(env, indices[index], &element), + "Failed to create pattern index") || + !Check(env, napi_set_element(env, result, index, element), + "Failed to write pattern index")) { + return nullptr; + } } - return result; - } catch (const std::exception& e) { - napi_throw_error(env, nullptr, e.what()); + } catch (const std::exception& error) { + napi_throw_error(env, nullptr, error.what()); return nullptr; } } -NAPI_INIT() { - NAPI_EXPORT_FUNCTION(regex_init); - NAPI_EXPORT_FUNCTION(regex_test); - NAPI_EXPORT_FUNCTION(set_init); - NAPI_EXPORT_FUNCTION(set_test); +bool ExportFunction(napi_env env, napi_value exports, const char* name, + napi_callback callback) { + napi_value function; + return Check(env, + napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, + nullptr, &function), + "Failed to create native function") && + Check(env, napi_set_named_property(env, exports, name, function), + "Failed to export native function"); +} + +} // namespace + +NAPI_MODULE_INIT() { + if (!ExportFunction(env, exports, "regex_init", RegexInit) || + !ExportFunction(env, exports, "regex_test", RegexTest) || + !ExportFunction(env, exports, "set_init", SetInit) || + !ExportFunction(env, exports, "set_test", SetTest)) { + return nullptr; + } + return exports; } diff --git a/binding.gyp b/binding.gyp index af79308..aabc10d 100644 --- a/binding.gyp +++ b/binding.gyp @@ -24,7 +24,6 @@ "vendor/re2/re2/tostring.cc", "vendor/re2/re2/unicode_casefold.cc", "vendor/re2/re2/unicode_groups.cc", - "vendor/re2/util/pcre.cc", "vendor/re2/util/rune.cc", "vendor/re2/util/strutil.cc", "vendor/abseil-cpp/absl/base/internal/cycleclock.cc", @@ -37,34 +36,35 @@ "vendor/abseil-cpp/absl/base/internal/thread_identity.cc", "vendor/abseil-cpp/absl/base/internal/throw_delegate.cc", "vendor/abseil-cpp/absl/base/internal/unscaledcycleclock.cc", - "vendor/abseil-cpp/absl/debugging/internal/demangle.cc", "vendor/abseil-cpp/absl/container/internal/raw_hash_set.cc", + "vendor/abseil-cpp/absl/container/internal/hashtablez_sampler.cc", + "vendor/abseil-cpp/absl/container/internal/hashtablez_sampler_force_weak_definition.cc", "vendor/abseil-cpp/absl/debugging/internal/address_is_readable.cc", + "vendor/abseil-cpp/absl/debugging/internal/decode_rust_punycode.cc", + "vendor/abseil-cpp/absl/debugging/internal/demangle.cc", + "vendor/abseil-cpp/absl/debugging/internal/demangle_rust.cc", "vendor/abseil-cpp/absl/debugging/internal/elf_mem_image.cc", "vendor/abseil-cpp/absl/debugging/internal/examine_stack.cc", + "vendor/abseil-cpp/absl/debugging/internal/utf8_for_code_point.cc", "vendor/abseil-cpp/absl/debugging/internal/vdso_support.cc", + "vendor/abseil-cpp/absl/debugging/leak_check.cc", "vendor/abseil-cpp/absl/debugging/stacktrace.cc", "vendor/abseil-cpp/absl/debugging/symbolize.cc", - "vendor/abseil-cpp/absl/flags/commandlineflag.cc", - "vendor/abseil-cpp/absl/flags/internal/commandlineflag.cc", - "vendor/abseil-cpp/absl/flags/internal/flag.cc", - "vendor/abseil-cpp/absl/flags/internal/private_handle_accessor.cc", - "vendor/abseil-cpp/absl/flags/internal/program_name.cc", - "vendor/abseil-cpp/absl/flags/marshalling.cc", - "vendor/abseil-cpp/absl/flags/reflection.cc", - "vendor/abseil-cpp/absl/flags/usage_config.cc", "vendor/abseil-cpp/absl/hash/internal/city.cc", "vendor/abseil-cpp/absl/hash/internal/hash.cc", "vendor/abseil-cpp/absl/hash/internal/print_hash_of.cc", + "vendor/abseil-cpp/absl/log/internal/check_op.cc", "vendor/abseil-cpp/absl/log/internal/globals.cc", "vendor/abseil-cpp/absl/log/internal/log_format.cc", "vendor/abseil-cpp/absl/log/internal/log_message.cc", "vendor/abseil-cpp/absl/log/internal/log_sink_set.cc", "vendor/abseil-cpp/absl/log/internal/nullguard.cc", "vendor/abseil-cpp/absl/log/internal/proto.cc", + "vendor/abseil-cpp/absl/log/internal/structured_proto.cc", "vendor/abseil-cpp/absl/log/globals.cc", "vendor/abseil-cpp/absl/log/log_sink.cc", "vendor/abseil-cpp/absl/numeric/int128.cc", + "vendor/abseil-cpp/absl/profiling/internal/exponential_biased.cc", "vendor/abseil-cpp/absl/strings/ascii.cc", "vendor/abseil-cpp/absl/strings/charconv.cc", "vendor/abseil-cpp/absl/strings/internal/charconv_bigint.cc", @@ -81,12 +81,17 @@ "vendor/abseil-cpp/absl/strings/str_cat.cc", "vendor/abseil-cpp/absl/strings/str_split.cc", "vendor/abseil-cpp/absl/strings/string_view.cc", + "vendor/abseil-cpp/absl/strings/internal/utf8.cc", "vendor/abseil-cpp/absl/synchronization/internal/create_thread_identity.cc", "vendor/abseil-cpp/absl/synchronization/internal/graphcycles.cc", "vendor/abseil-cpp/absl/synchronization/internal/futex_waiter.cc", "vendor/abseil-cpp/absl/synchronization/internal/kernel_timeout.cc", "vendor/abseil-cpp/absl/synchronization/internal/per_thread_sem.cc", + "vendor/abseil-cpp/absl/synchronization/internal/pthread_waiter.cc", + "vendor/abseil-cpp/absl/synchronization/internal/sem_waiter.cc", + "vendor/abseil-cpp/absl/synchronization/internal/stdcpp_waiter.cc", "vendor/abseil-cpp/absl/synchronization/internal/waiter_base.cc", + "vendor/abseil-cpp/absl/synchronization/internal/win32_waiter.cc", "vendor/abseil-cpp/absl/synchronization/mutex.cc", "vendor/abseil-cpp/absl/time/clock.cc", "vendor/abseil-cpp/absl/time/duration.cc", @@ -101,47 +106,43 @@ "vendor/abseil-cpp/absl/time/time.cc", ], "cflags": [ - "-fexceptions", - "-std=c++2a", "-Wall", "-Wextra", "-Wno-sign-compare", "-Wno-unused-parameter", "-Wno-missing-field-initializers", - "-Wno-cast-function-type", - "-O3", - "-march=znver1", - "-g" + "-Wno-cast-function-type" ], 'cflags_cc': [ - "-fexceptions", - "-march=znver1", + "-std=c++20", + "-fexceptions" ], "defines": [ - "NDEBUG", "NOMINMAX" ], "include_dirs": [ - " /dev/null 2>&1 || true + fi + rm -f "$IMAGE_ID_FILE" +} +trap cleanup EXIT + +if [ ! -e vendor/abseil-cpp/.git ] || [ ! -e vendor/re2/.git ]; then + git submodule update --init --recursive +fi + +DOCKER_ARGS=(--platform "$PLATFORM") +if [ -n "${JOBS:-}" ]; then + DOCKER_ARGS+=(--build-arg "JOBS=$JOBS") +fi + +docker build \ + "${DOCKER_ARGS[@]}" \ + --iidfile "$IMAGE_ID_FILE" \ + . + +IMAGE_ID=$(cat "$IMAGE_ID_FILE") +CONTAINER_ID=$(docker create --platform "$PLATFORM" "$IMAGE_ID") + +rm -rf prebuilds/linux-x64 +mkdir -p prebuilds +docker cp "$CONTAINER_ID:/node-re2/prebuilds/linux-x64" prebuilds/ + +echo "Built prebuilds/linux-x64." diff --git a/index.d.ts b/index.d.ts index 1046797..73aef01 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,15 +1,17 @@ +export type BinaryView = ArrayBufferView; + export class RE2 { - constructor(pattern: string | Buffer); + constructor(pattern: string | BinaryView); /** * @returns True if the pattern matches, false otherwise. */ - test(buffer: Buffer, byteOffset?: number, byteLength?: number): boolean; + test(buffer: BinaryView, byteOffset?: number, byteLength?: number): boolean; } export class RE2Set { - constructor(patterns: (string | Buffer)[]); + constructor(patterns: readonly (string | BinaryView)[]); /** - * @returns An array of the indices of the patterns that matched, or an empty array if no patterns matched. + * @returns The indices of the matching patterns in unspecified order, or an empty array if no patterns matched. */ - test(buffer: Buffer, byteOffset?: number, byteLength?: number): number[]; + test(buffer: BinaryView, byteOffset?: number, byteLength?: number): number[]; } diff --git a/index.js b/index.js index 94fa02d..aaa93d7 100644 --- a/index.js +++ b/index.js @@ -1,22 +1,46 @@ +import { Buffer } from 'node:buffer' import binding from './binding.js' +/** + * @param {unknown} value + * @param {string} name + * @returns {NodeJS.ArrayBufferView} + */ +function asBinaryView(value, name) { + if (!ArrayBuffer.isView(value)) { + throw new TypeError(`${name} must be a Buffer, TypedArray, or DataView`) + } + return value +} + +/** + * @param {unknown} pattern + * @returns {NodeJS.ArrayBufferView} + */ +function encodePattern(pattern) { + return typeof pattern === 'string' + ? Buffer.from(pattern) + : asBinaryView(pattern, 'pattern') +} + export class RE2 { #context /** - * @param {string | Buffer} pattern + * @param {string | NodeJS.ArrayBufferView} pattern */ constructor(pattern) { - this.#context = binding.regex_init(typeof pattern === 'string' ? Buffer.from(pattern) : pattern) + this.#context = binding.regex_init(encodePattern(pattern)) } /** - * @param {Buffer} buffer + * @param {NodeJS.ArrayBufferView} buffer * @param {number} [byteOffset] * @param {number} [byteLength] * @returns {boolean} True if the pattern matches, false otherwise. */ test(buffer, byteOffset, byteLength) { + buffer = asBinaryView(buffer, 'buffer') if (byteOffset === undefined) { byteOffset = 0 } @@ -31,19 +55,23 @@ export class RE2Set { #context /** - * @param {(string | Buffer)[]} patterns + * @param {readonly (string | NodeJS.ArrayBufferView)[]} patterns */ constructor(patterns) { - this.#context = binding.set_init(patterns.map(pattern => typeof pattern === 'string' ? Buffer.from(pattern) : pattern)) + if (!Array.isArray(patterns)) { + throw new TypeError('patterns must be an array') + } + this.#context = binding.set_init(Array.from(patterns, encodePattern)) } /** - * @param {Buffer} buffer + * @param {NodeJS.ArrayBufferView} buffer * @param {number} [byteOffset] * @param {number} [byteLength] - * @returns {number[]} An array of the indices of the patterns that matched, or an empty array if no patterns matched. + * @returns {number[]} The indices of the matching patterns in unspecified order, or an empty array if no patterns matched. */ test(buffer, byteOffset, byteLength) { + buffer = asBinaryView(buffer, 'buffer') if (byteOffset === undefined) { byteOffset = 0 } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f66d74a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,954 @@ +{ + "name": "@nxtedition/re2", + "version": "1.0.11", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nxtedition/re2", + "version": "1.0.11", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp": "^13.0.1", + "node-gyp-build": "^4.8.4" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "prebuildify": "^6.0.1", + "typescript": "^7.0.2" + }, + "engines": { + "node": ">=26" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/abbrev": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-13.0.1.tgz", + "integrity": "sha512-piOr0S10qy5THB+q5BdqkoOx65XL/tjTMUAit3vciPNp+snTOBnGunWH1Rz7XZUxf2T9uFrfT/Ty4+aC3yPeyg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^10.0.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^8.4.1", + "which": "^7.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nopt": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", + "license": "ISC", + "dependencies": { + "abbrev": "^5.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", + "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuildify": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/prebuildify/-/prebuildify-6.0.1.tgz", + "integrity": "sha512-8Y2oOOateom/s8dNBsGIcnm6AxPmLH4/nanQzL5lQMU+sC0CMhzARZHizwr36pUPLdvBnOkCNQzxg4djuFSgIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "mkdirp-classic": "^0.5.3", + "node-abi": "^3.3.0", + "npm-run-path": "^3.1.0", + "pump": "^3.0.0", + "tar-fs": "^2.1.0" + }, + "bin": { + "prebuildify": "bin.js" + } + }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar": { + "version": "7.5.20", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz", + "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz", + "integrity": "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index b5c455a..61dd6da 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,83 @@ { "name": "@nxtedition/re2", "version": "1.0.11", + "description": "Fast RE2 partial matching for Node.js", "type": "module", "main": "index.js", "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "require": "./index.js", + "default": "./index.js" + } + }, + "files": [ + "binding.cc", + "binding.gyp", + "binding.js", + "index.d.ts", + "index.js", + "prebuilds", + "THIRD_PARTY.md", + "vendor/abseil-cpp/absl", + "vendor/abseil-cpp/LICENSE", + "vendor/re2/re2", + "vendor/re2/util/rune.cc", + "vendor/re2/util/strutil.cc", + "vendor/re2/util/strutil.h", + "vendor/re2/util/utf.h", + "vendor/re2/LICENSE", + "!vendor/**/testdata/**", + "!vendor/**/testing/**", + "!vendor/**/*_test.c", + "!vendor/**/*_test.cc", + "!vendor/**/*_test.h", + "!vendor/**/test_*.c", + "!vendor/**/test_*.cc", + "!vendor/**/test_*.h", + "!vendor/**/*_benchmark.cc", + "!vendor/**/*_benchmark.h", + "!vendor/**/benchmarks.cc", + "!vendor/**/*_test*.*", + "!vendor/**/*_testing*.*", + "!vendor/**/*_benchmark*.*", + "!vendor/**/*_benchmarks*.*", + "!vendor/**/CMakeLists.txt", + "!vendor/**/BUILD", + "!vendor/**/BUILD.bazel" + ], + "engines": { + "node": ">=26" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nxtedition/node-re2.git" + }, + "bugs": { + "url": "https://github.com/nxtedition/node-re2/issues" + }, + "homepage": "https://github.com/nxtedition/node-re2#readme", "dependencies": { - "install-artifact-from-github": "^1.3.5", - "nan": "^2.20.0", - "napi-macros": "^2.2.2", - "node-gyp": "^10.2.0", - "node-gyp-build": "^4.8.2" + "node-gyp": "^13.0.1", + "node-gyp-build": "^4.8.4" }, "devDependencies": { - "@types/node": "^25.2.3" + "@types/node": "^26.1.1", + "prebuildify": "^6.0.1", + "typescript": "^7.0.2" }, "scripts": { "install": "node-gyp-build", - "test": "node --test", - "rebuild": "JOBS=8 npm run install --build-from-source" + "build": "node-gyp rebuild --jobs=max", + "pretest": "npm run build", + "test": "node --test && npm run test:types", + "test:prebuild": "PREBUILDS_ONLY=1 node --test && npm run test:types", + "test:types": "tsc --noEmit", + "verify:prebuilds": "node scripts/verify-prebuilds.mjs", + "rebuild": "npm_config_build_from_source=true node-gyp-build", + "release": "./release.sh" }, "gypfile": true, "license": "MIT" diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..b6c8d3f --- /dev/null +++ b/release.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" + +if ! npm whoami --registry https://registry.npmjs.org > /dev/null 2>&1; then + echo "Not logged in to npm; run 'npm login' first." >&2 + exit 1 +fi + +if [ -n "$(git status --porcelain)" ]; then + echo "Working tree is not clean; commit or stash changes first." >&2 + exit 1 +fi + +BRANCH=$(git rev-parse --abbrev-ref HEAD) +git fetch origin "$BRANCH" + +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse "origin/$BRANCH") +BASE=$(git merge-base HEAD "origin/$BRANCH") + +if [ "$LOCAL" = "$REMOTE" ] || [ "$REMOTE" = "$BASE" ]; then + : +elif [ "$LOCAL" = "$BASE" ]; then + echo "Branch '$BRANCH' is behind origin; pull first." >&2 + exit 1 +else + echo "Branch '$BRANCH' has diverged from origin; reconcile first." >&2 + exit 1 +fi + +NODE_TARGET=$(sed -n 's/^FROM node:\([0-9.]*\).*/\1/p' Dockerfile) +if [ -z "$NODE_TARGET" ]; then + echo "Could not determine the Node target from Dockerfile." >&2 + exit 1 +fi + +if [ "$(uname -s)" != "Darwin" ] || [ "$(uname -m)" != "arm64" ]; then + echo "The darwin-arm64 prebuild must be produced on an arm64 Mac." >&2 + exit 1 +fi + +rm -rf prebuilds + +echo "Building linux-x64 prebuild (Docker)..." +./build.sh + +echo "Building darwin-arm64 prebuild (Node $NODE_TARGET)..." +JOBS=${JOBS:-8} npx prebuildify \ + -t "$NODE_TARGET" \ + --napi \ + --strip \ + --arch arm64 + +npm run test:prebuild +npm run verify:prebuilds + +read -r -p "Version bump (patch/minor/major): " BUMP +case "$BUMP" in + patch | minor | major) ;; + *) + echo "Invalid bump: '$BUMP' (expected patch, minor, or major)." >&2 + exit 1 + ;; +esac + +npm version "$BUMP" +npm publish --registry https://registry.npmjs.org +git push +git push --tags + +echo "Published $(node -p "require('./package.json').version")." diff --git a/scripts/verify-prebuilds.mjs b/scripts/verify-prebuilds.mjs new file mode 100644 index 0000000..c14f4c4 --- /dev/null +++ b/scripts/verify-prebuilds.mjs @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import { existsSync, readdirSync } from 'node:fs' + +const required = [ + 'prebuilds/linux-x64/@nxtedition+re2.glibc.node', + 'prebuilds/darwin-arm64/@nxtedition+re2.node', +] + +for (const path of required) { + assert.ok(existsSync(path), `missing ${path}`) +} + +assert.deepEqual( + readdirSync('prebuilds/linux-x64').filter((path) => path.endsWith('.node')), + ['@nxtedition+re2.glibc.node'], + 'linux-x64 must contain only the libc-tagged glibc prebuild', +) + +const [pack] = JSON.parse( + execFileSync('npm', ['pack', '--dry-run', '--json'], { + encoding: 'utf8', + env: { ...process.env, npm_config_ignore_scripts: 'true' }, + }), +) +const packedFiles = new Set(pack.files.map(({ path }) => path)) + +for (const path of required) { + assert.ok(packedFiles.has(path), `${path} is missing from the npm tarball`) +} + +console.log('Verified packaged linux-x64 and darwin-arm64 N-API prebuilds.') diff --git a/test.js b/test.js index 956fca0..548d3b0 100644 --- a/test.js +++ b/test.js @@ -1,15 +1,166 @@ -import { test } from 'node:test' -import assert from 'node:assert' -import { RE2, RE2Set } from './index.js' +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { describe, test } from 'node:test' +import { RE2, RE2Set } from '@nxtedition/re2' +import nodeGypBuild from 'node-gyp-build' -test('RE2', () => { - const expr = new RE2('foo') - assert(expr.test(Buffer.from('foo'))) +const toSortedIndices = indices => indices.toSorted((left, right) => left - right) + +describe('RE2', () => { + test('performs partial matches', () => { + const expression = new RE2('foo') + + assert.equal(expression.test(Buffer.from('before foo after')), true) + assert.equal(expression.test(Buffer.from('bar')), false) + }) + + test('supports empty and binary patterns', () => { + assert.equal(new RE2('').test(Buffer.alloc(0)), true) + assert.equal(new RE2(new Uint8Array()).test(new DataView(new ArrayBuffer(0))), true) + assert.equal( + new RE2(Buffer.from([0x00, 0x62])).test(Buffer.from([0x61, 0x00, 0x62, 0x63])), + true + ) + }) + + test('copies pattern data at construction', () => { + const pattern = Buffer.from('foo') + const expression = new RE2(pattern) + pattern.fill(0x78) + + assert.equal(expression.test(Buffer.from('foo')), true) + assert.equal(expression.test(Buffer.from('xxx')), false) + }) + + test('rejects invalid patterns without native log noise', () => { + for (const pattern of ['(', '(a)\\1', Buffer.from([0xff])]) { + assert.throws(() => new RE2(pattern), Error) + } + + const moduleUrl = new URL('./index.js', import.meta.url).href + const child = spawnSync( + process.execPath, + ['--input-type=module', '--eval', `import { RE2 } from ${JSON.stringify(moduleUrl)}; try { new RE2('(') } catch {}`], + { encoding: 'utf8' } + ) + assert.equal(child.status, 0, child.stderr) + assert.equal(child.stderr, '') + }) + + test('supports Buffer, TypedArray, DataView, and shared views', () => { + const bytes = new Uint8Array([0x78, 0x66, 0x6f, 0x6f, 0x79]) + const typedArray = bytes.subarray(1, 4) + const dataView = new DataView(bytes.buffer, 1, 3) + const sharedBuffer = new SharedArrayBuffer(3) + const sharedView = new Uint8Array(sharedBuffer) + sharedView.set(typedArray) + + assert.equal(new RE2(typedArray).test(Buffer.from('foo')), true) + assert.equal(new RE2(dataView).test(typedArray), true) + assert.equal(new RE2('foo').test(sharedView), true) + assert.equal( + new RE2('cd').test(new Uint16Array(new Uint8Array([0x61, 0x62, 0x63, 0x64]).buffer)), + true + ) + assert.throws(() => new RE2(new ArrayBuffer(3)), TypeError) + assert.throws(() => new RE2('foo').test('foo'), TypeError) + }) + + test('matches byte ranges and clamps them without 32-bit wrapping', () => { + const buffer = Buffer.from('xfooy') + const exact = new RE2('^foo$') + + assert.equal(exact.test(buffer, 1, 3), true) + assert.equal(exact.test(buffer, 0, 4), false) + assert.equal(new RE2('^xfoo$').test(buffer, -10, 4), true) + assert.equal(exact.test(buffer, 1.9, 3.9), true) + assert.equal(new RE2('x').test(buffer, 2 ** 31, 1), false) + assert.equal(new RE2('x').test(buffer, 2 ** 32, 1), false) + assert.equal(new RE2('x').test(buffer, Number.MAX_SAFE_INTEGER, 1), false) + assert.equal(new RE2('^xfooy$').test(buffer, 0, 2 ** 32), true) + assert.equal(new RE2('^x$').test(buffer, Number.NaN, 1), true) + assert.equal(new RE2('^x$').test(buffer, Number.POSITIVE_INFINITY, 1), false) + assert.equal(new RE2('^xfooy$').test(buffer, 0, Number.POSITIVE_INFINITY), true) + assert.throws(() => exact.test(buffer, '1', 3), TypeError) + }) + + test('uses byte offsets for Unicode input', () => { + const buffer = Buffer.from('a💩b') + const expression = new RE2('^💩$') + + assert.equal(expression.test(buffer, 1, 4), true) + assert.equal(expression.test(buffer, 2, 3), false) + }) +}) + +describe('RE2Set', () => { + test('returns all matching indices and an empty array for misses', () => { + const expressions = new RE2Set(['foo', 'o', 'bar']) + + assert.deepEqual(toSortedIndices(expressions.test(Buffer.from('foo'))), [0, 1]) + assert.deepEqual(expressions.test(Buffer.from('baz')), []) + }) + + test('supports empty sets, duplicates, and empty patterns', () => { + assert.deepEqual(new RE2Set([]).test(Buffer.alloc(0)), []) + assert.deepEqual( + toSortedIndices(new RE2Set(['foo', 'foo']).test(Buffer.from('foo'))), + [0, 1] + ) + assert.deepEqual(new RE2Set(['']).test(Buffer.alloc(0)), [0]) + }) + + test('rejects invalid patterns', () => { + for (const pattern of ['(', '(a)\\1', Buffer.from([0xff])]) { + assert.throws(() => new RE2Set(['valid', pattern]), Error) + } + }) + + test('normalizes Array subclasses and validates constructor input', () => { + class PatternArray extends Array { + static get [Symbol.species]() { + return Object + } + } + + const patterns = new PatternArray('foo', 'bar') + const expressions = new RE2Set(patterns) + assert.deepEqual(expressions.test(Buffer.from('foo')), [0]) + assert.throws(() => new RE2Set('foo'), TypeError) + }) + + test('supports binary views and the same byte-range semantics as RE2', () => { + const bytes = new Uint8Array([0x78, 0x66, 0x6f, 0x6f, 0x79]) + const patterns = [new DataView(bytes.buffer, 1, 3), 'bar'] + const expressions = new RE2Set(patterns) + + assert.deepEqual(expressions.test(bytes, 1, 3), [0]) + assert.deepEqual(expressions.test(bytes, 2 ** 32, 1), []) + }) +}) + +test('supports synchronous CommonJS loading', () => { + const require = createRequire(import.meta.url) + const commonjs = require('@nxtedition/re2') + + assert.equal(new commonjs.RE2('foo').test(Buffer.from('foo')), true) }) +test('native addon has no unresolved vendored symbols', { + skip: process.platform === 'win32' ? 'nm is not available on Windows' : false +}, () => { + const bindingPath = nodeGypBuild.path(import.meta.dirname) + const symbols = spawnSync('nm', ['-u', bindingPath], { encoding: 'utf8' }) + assert.equal(symbols.status, 0, symbols.stderr) -test('Set', () => { - const expr = new RE2Set(['foo', 'o', 'bar']) - assert.deepStrictEqual(expr.test(Buffer.from('foo')), [1, 0]) - assert.deepStrictEqual(expr.test(Buffer.from('baz')), []) + const demangled = spawnSync('c++filt', { input: symbols.stdout, encoding: 'utf8' }) + assert.equal(demangled.status, 0, demangled.stderr) + const unresolvedVendoredSymbols = demangled.stdout + .split('\n') + .filter(line => + /(?:absl|re2)::/.test(line) && + !/^\s*w TLS init function for re2::hooks::context$/.test(line) + ) + assert.deepEqual(unresolvedVendoredSymbols, []) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..70aaf1b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "target": "ES2024", + "types": ["node"], + "verbatimModuleSyntax": true + }, + "include": ["index.d.ts", "type-tests/**/*.ts"] +} diff --git a/type-tests/index.ts b/type-tests/index.ts new file mode 100644 index 0000000..0f399db --- /dev/null +++ b/type-tests/index.ts @@ -0,0 +1,26 @@ +import { Buffer } from 'node:buffer' +import { RE2, RE2Set, type BinaryView } from '@nxtedition/re2' + +const buffer = Buffer.from('foo') +const typedArray = new Uint8Array(buffer) +const dataView = new DataView(typedArray.buffer) +const sharedView = new Uint8Array(new SharedArrayBuffer(3)) +const views: readonly BinaryView[] = [buffer, typedArray, dataView, sharedView] + +const expression = new RE2(typedArray) +const expressions = new RE2Set(['foo', buffer, dataView] as const) + +for (const view of views) { + const matches: boolean = expression.test(view, 0, view.byteLength) + const indices: number[] = expressions.test(view) + void [matches, indices] +} + +// @ts-expect-error patterns must be strings or binary views +new RE2(42) +// @ts-expect-error raw ArrayBuffers are not views +new RE2(new ArrayBuffer(3)) +// @ts-expect-error input must be a binary view +expression.test('foo') +// @ts-expect-error RE2Set requires an array +new RE2Set('foo') diff --git a/vendor/abseil-cpp b/vendor/abseil-cpp index 8e77675..0cf0a5c 160000 --- a/vendor/abseil-cpp +++ b/vendor/abseil-cpp @@ -1 +1 @@ -Subproject commit 8e7767542cfde43f16c142f05dce06cadb7f7f84 +Subproject commit 0cf0a5c9d12cc3783363ab20f11613e69fd04c9a diff --git a/vendor/re2 b/vendor/re2 index 6569a9a..927f5d5 160000 --- a/vendor/re2 +++ b/vendor/re2 @@ -1 +1 @@ -Subproject commit 6569a9a3df256f4c0c3813cb8ee2f8eef6e2c1fb +Subproject commit 927f5d53caf8111721e734cf24724686bb745f55